diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 515c4142ca52..073283cf3cdf 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -794,25 +794,6 @@ declare namespace Cypress { onSpecWindow: (window: Window, specList: string[] | Array<() => Promise>) => void } - interface SessionOptions { - /** - * Whether or not to persist the session across all specs in the run. - * @default {false} - */ - cacheAcrossSpecs?: boolean - /** - * Function to run immediately after the session is created and `setup` function runs or - * after a session is restored and the page is cleared. If this returns `false`, throws an - * exception, returns a Promise which resolves to `false` or rejects or contains any failing - * Cypress command, the session is considered invalid. - * - * If validation fails immediately after `setup`, the test will fail. - * If validation fails after restoring a session, `setup` will re-run. - * @default {false} - */ - validate?: () => Promise | void - } - type CanReturnChainable = void | Chainable | Promise type ThenReturn = R extends void ? Chainable : @@ -3423,8 +3404,59 @@ declare namespace Cypress { } interface Session { - // Clear all saved sessions and re-run the current spec file. + /** + * Clear all sessions saved on the backend, including cached global sessions. + */ clearAllSavedSessions: () => Promise + /** + * Clear all storage and cookie data across all origins associated with the current session. + */ + clearCurrentSessionData: () => Promise + /** + * Get all storage and cookie data across all origins associated with the current session. + */ + getCurrentSessionData: () => Promise + /** + * Get all storage and cookie data saved on the backend associated with the provided session id. + */ + getSession: (id: string) => Promise + } + + type ActiveSessions = Record + + interface SessionData { + id: string + hydrated: boolean + cacheAcrossSpecs: SessionOptions['cacheAcrossSpecs'] + cookies?: Cookie[] | null + localStorage?: OriginStorage[] | null + sessionStorage?: OriginStorage[] | null + setup: () => void + validate?: SessionOptions['validate'] + } + + interface ServerSessionData extends Omit { + setup: string + validate?: string + } + + interface SessionOptions { + /** + * Whether or not to persist the session across all specs in the run. + * @default {false} + */ + cacheAcrossSpecs?: boolean + /** + * Function to run immediately after the session is created and `setup` function runs or + * after a session is restored and the page is cleared. If this returns `false`, throws an + * exception, returns a Promise which resolves to `false` or rejects or contains any failing + * Cypress command, the session is considered invalid. + * + * If validation fails immediately after `setup`, the test will fail. + * If validation fails after restoring a session, `setup` will re-run. + * @default {false} + */ + validate?: () => Promise | void } type SameSiteStatus = 'no_restriction' | 'strict' | 'lax' diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index c17eb3aa5b01..c6435e12ee4d 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -354,7 +354,7 @@ describe('runner/cypress sessions.ui.spec', { before(() => { cy.then(async () => { await Cypress.action('cy:url:changed', '') - await Cypress.action('cy:visit:blank', { type: 'on' }) + await Cypress.action('cy:visit:blank', { testIsolation: false }) }) .then(() => { loadSpec({ @@ -550,7 +550,7 @@ describe('runner/cypress sessions.ui.spec', { before(() => { cy.then(async () => { await Cypress.action('cy:url:changed', '') - await Cypress.action('cy:visit:blank', { type: 'on' }) + await Cypress.action('cy:visit:blank', { testIsolation: false }) }) .then(() => { loadSpec({ @@ -630,7 +630,7 @@ describe('runner/cypress sessions.ui.spec', { before(() => { cy.then(async () => { await Cypress.action('cy:url:changed', '') - await Cypress.action('cy:visit:blank', { type: 'on' }) + await Cypress.action('cy:visit:blank', { testIsolation: false }) }) .then(() => { loadSpec({ diff --git a/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts index 68a9412e5a73..c6072a84e0f0 100644 --- a/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts +++ b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts @@ -22,7 +22,7 @@ describe('src/cy/commands/sessions/manager.ts', () => { it('adds session when none were previously added', () => { const cySpy = cy.spy(cy, 'state').withArgs('activeSessions') - const activeSession: Cypress.Commands.Session.ActiveSessions = { + const activeSession: Cypress.ActiveSessions = { 'session_1': { id: 'session_1', setup: () => {}, @@ -42,7 +42,7 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) it('adds session when other sessions were previously added', () => { - const existingSessions: Cypress.Commands.Session.ActiveSessions = { + const existingSessions: Cypress.ActiveSessions = { 'session_1': { id: 'session_1', setup: () => {}, @@ -59,7 +59,7 @@ describe('src/cy/commands/sessions/manager.ts', () => { const cySpy = cy.stub(cy, 'state').callThrough().withArgs('activeSessions').returns(existingSessions) - const activeSession: Cypress.Commands.Session.ActiveSessions = { + const activeSession: Cypress.ActiveSessions = { 'session_3': { id: 'session_3', setup: () => {}, @@ -94,7 +94,7 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) it('returns session when found', () => { - const activeSessions: Cypress.Commands.Session.ActiveSessions = { + const activeSessions: Cypress.ActiveSessions = { 'session_1': { id: 'session_1', setup: () => {}, @@ -135,7 +135,7 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) it('updates the existing active sessions to "hydrated: false"', () => { - const existingSessions: Cypress.Commands.Session.ActiveSessions = { + const existingSessions: Cypress.ActiveSessions = { 'session_1': { id: 'session_1', setup: () => {}, @@ -166,24 +166,41 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) }) - describe('.sessions', () => { - it('sessions.defineSession()', () => { - const sessionsManager = new SessionsManager(CypressInstance, cy) - const setup = cy.stub() - const sess = sessionsManager.sessions.defineSession({ id: '1', setup }) - - expect(sess).to.deep.eq({ - id: '1', - setup, - validate: undefined, - cookies: null, - cacheAcrossSpecs: false, - localStorage: null, - sessionStorage: null, - hydrated: false, - }) + it('.defineSession()', () => { + const sessionsManager = new SessionsManager(CypressInstance, cy) + const setup = cy.stub() + const sess = sessionsManager.defineSession({ id: '1', setup }) + + expect(sess).to.deep.eq({ + id: '1', + setup, + validate: undefined, + cookies: null, + cacheAcrossSpecs: false, + localStorage: null, + sessionStorage: null, + hydrated: false, }) + }) + + it('.saveSessionData()', async () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('save:session').resolves(null) + + const sessionsManager = new SessionsManager(CypressInstance, cy) + const sessionsSpy = cy.stub(sessionsManager, 'setActiveSession') + + const setup = cy.stub() + const sess = { id: '1', setup } + + await sessionsManager.saveSessionData(sess) + + expect(sessionsSpy).to.be.calledOnce + expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess }) + + expect(cypressSpy).to.be.calledOnceWith('save:session') + }) + describe('.sessions', () => { it('sessions.clearAllSavedSessions()', async () => { const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:sessions', true).resolves(null) @@ -260,26 +277,6 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) }) - it('sessions.saveSessionData', async () => { - const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('save:session').resolves(null) - - const sessionsManager = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessionsManager, 'setActiveSession') - - const setup = cy.stub() - const sess = { id: '1', setup } - - await sessionsManager.sessions.saveSessionData(sess) - - expect(sessionsSpy).to.be.calledOnce - expect(sessionsSpy.getCall(0).args[0]).to.deep.eq({ 1: sess }) - - expect(cypressSpy).to.be.calledOnceWith('save:session') - }) - - // TODO: - describe('sessions.setSessionData', () => {}) - it('sessions.getCookies()', async () => { const cookies = [{ id: 'cookie' }] const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) diff --git a/packages/driver/package.json b/packages/driver/package.json index 9dc0194be7c5..163a11b32abc 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -16,6 +16,7 @@ "@babel/code-frame": "7.8.3", "@cypress/sinon-chai": "2.9.1", "@cypress/unique-selector": "0.4.4", + "@cypress/webpack-dev-server": "0.0.0-development", "@cypress/webpack-preprocessor": "0.0.0-development", "@cypress/what-is-circular": "1.0.1", "@packages/config": "0.0.0-development", diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index 0d23217e23b4..32c1fa680f9a 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -12,10 +12,6 @@ import { statusMap, } from './utils' -import type { ServerSessionData } from '@packages/types' - -type SessionData = Cypress.Commands.Session.SessionData - /** * Session data should be cleared with spec browser launch. * @@ -30,7 +26,7 @@ export default function (Commands, Cypress, cy) { Cypress.on('run:start', () => { // @ts-ignore - Object.values(Cypress.state('activeSessions') || {}).forEach((sessionData: ServerSessionData) => { + Object.values(Cypress.state('activeSessions') || {}).forEach((sessionData: Cypress.ServerSessionData) => { if (sessionData.cacheAcrossSpecs) { sessionsManager.registeredSessions.set(sessionData.id, true) } @@ -89,7 +85,7 @@ export default function (Commands, Cypress, cy) { }) } - let session: SessionData = sessionsManager.getActiveSession(id) + let session: Cypress.SessionData = sessionsManager.getActiveSession(id) const isRegisteredSessionForSpec = sessionsManager.registeredSessions.has(id) if (session) { @@ -120,7 +116,7 @@ export default function (Commands, Cypress, cy) { $errUtils.throwErrByPath('sessions.session.duplicateId', { args: { id } }) } - session = sessions.defineSession({ + session = sessionsManager.defineSession({ id, setup, validate: options.validate, @@ -215,7 +211,7 @@ export default function (Commands, Cypress, cy) { _log.set({ consoleProps: () => getConsoleProps(testSession) }) - return sessions.setSessionData(testSession) + return sessionsManager.setSessionData(testSession) } function validateSession (existingSession, step: keyof typeof SESSION_STEPS) { @@ -428,7 +424,7 @@ export default function (Commands, Cypress, cy) { } sessionsManager.registeredSessions.set(existingSession.id, true) - await sessions.saveSessionData(existingSession) + await sessionsManager.saveSessionData(existingSession) return statusMap.complete(step) }) @@ -440,7 +436,7 @@ export default function (Commands, Cypress, cy) { * 2. validate session * 3. if validation fails, catch error and recreate session */ - const restoreSessionWorkflow = (existingSession: SessionData) => { + const restoreSessionWorkflow = (existingSession: Cypress.SessionData) => { return cy.then(async () => { setSessionLogStatus(statusMap.inProgress(SESSION_STEPS.restore)) await navigateAboutBlank() diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 164d69a33028..c84c256c6aa5 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -1,12 +1,7 @@ -import type { ServerSessionData } from '@packages/types' import _ from 'lodash' - import { getAllHtmlOrigins } from './origins' import { clearStorage, getStorage, setStorage } from './storage' -type ActiveSessions = Cypress.Commands.Session.ActiveSessions -type SessionData = Cypress.Commands.Session.SessionData - const LOGS = { clearCurrentSessionData: { displayName: 'Clear cookies, localStorage and sessionStorage', @@ -39,7 +34,7 @@ export default class SessionsManager { this.cy = cy } - setActiveSession = (obj: ActiveSessions) => { + setActiveSession = (obj: Cypress.ActiveSessions) => { const currentSessions = this.cy.state('activeSessions') || {} const newSessions = { ...currentSessions, ...obj } @@ -47,7 +42,7 @@ export default class SessionsManager { this.cy.state('activeSessions', newSessions) } - getActiveSession = (id: string): SessionData => { + getActiveSession = (id: string): Cypress.SessionData => { const currentSessions = this.cy.state('activeSessions') || {} return currentSessions[id] @@ -55,26 +50,56 @@ export default class SessionsManager { clearActiveSessions = () => { const curSessions = this.cy.state('activeSessions') || {} - const clearedSessions: ActiveSessions = _.mapValues(curSessions, (v) => ({ ...v, hydrated: false })) + const clearedSessions: Cypress.ActiveSessions = _.mapValues(curSessions, (v) => ({ ...v, hydrated: false })) this.cy.state('activeSessions', clearedSessions) } - // this the public api exposed to consumers as Cypress.session - sessions = { - defineSession: (options = {} as any): SessionData => { - return { - id: options.id, - cookies: null, - localStorage: null, - sessionStorage: null, - setup: options.setup, - hydrated: false, - validate: options.validate, - cacheAcrossSpecs: !!options.cacheAcrossSpecs, + defineSession = (options = {} as any): Cypress.SessionData => { + return { + id: options.id, + cookies: null, + localStorage: null, + sessionStorage: null, + setup: options.setup, + hydrated: false, + validate: options.validate, + cacheAcrossSpecs: !!options.cacheAcrossSpecs, + } + } + + saveSessionData = async (data) => { + this.setActiveSession({ [data.id]: data }) + + // 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 this.Cypress.backend('save:session', { ...data, setup: data.setup.toString(), validate: data.validate?.toString() }).catch(console.error) + } + + setSessionData = async (data) => { + const allHtmlOrigins = await getAllHtmlOrigins(this.Cypress) + + 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([ + setStorage(this.Cypress, { localStorage: _localStorage, sessionStorage: _sessionStorage }), + this.sessions.setCookies(data.cookies), + ]) + } + + // this the public api exposed to consumers as Cypress.session + sessions = { clearAllSavedSessions: async () => { this.clearActiveSessions() this.registeredSessions.clear() @@ -98,36 +123,6 @@ export default class SessionsManager { ]) }, - saveSessionData: async (data) => { - this.setActiveSession({ [data.id]: data }) - - // 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 this.Cypress.backend('save:session', { ...data, setup: data.setup.toString(), validate: data.validate?.toString() }).catch(console.error) - }, - - setSessionData: async (data) => { - const allHtmlOrigins = await getAllHtmlOrigins(this.Cypress) - - 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([ - setStorage(this.Cypress, { localStorage: _localStorage, sessionStorage: _sessionStorage }), - this.sessions.setCookies(data.cookies), - ]) - }, - getCookies: async () => { return this.Cypress.automation('get:cookies', {}) }, @@ -152,7 +147,7 @@ export default class SessionsManager { } }, - getSession: (id: string): Promise => { + getSession: (id: string): Promise => { return this.Cypress.backend('get:session', id) }, } diff --git a/packages/driver/src/cy/commands/sessions/utils.ts b/packages/driver/src/cy/commands/sessions/utils.ts index 3892c674ecfb..dee035138ae2 100644 --- a/packages/driver/src/cy/commands/sessions/utils.ts +++ b/packages/driver/src/cy/commands/sessions/utils.ts @@ -3,9 +3,7 @@ import $ from 'jquery' import Bluebird from 'bluebird' import { $Location } from '../../../cypress/location' -type SessionData = Cypress.Commands.Session.SessionData - -const getSessionDetailsByDomain = (sessState: SessionData) => { +const getSessionDetailsByDomain = (sessState: Cypress.SessionData) => { return _.merge( _.mapValues(_.groupBy(sessState.cookies, 'domain'), (v) => ({ cookies: v })), ..._.map(sessState.localStorage, (v) => ({ [$Location.create(v.origin).hostname]: { localStorage: v } })), @@ -98,7 +96,7 @@ const setPostMessageLocalStorage = async (specWindow, originOptions) => { }) } -const getConsoleProps = (session: SessionData) => { +const getConsoleProps = (session: Cypress.SessionData) => { const sessionDetails = getSessionDetailsByDomain(session) const groupsByDomain = _.flatMap(sessionDetails, (val, domain) => { diff --git a/packages/driver/src/cypress/state.ts b/packages/driver/src/cypress/state.ts index a238e07a2c51..ccda98d84c6d 100644 --- a/packages/driver/src/cypress/state.ts +++ b/packages/driver/src/cypress/state.ts @@ -1,4 +1,3 @@ -/// /// import type Bluebird from 'bluebird' @@ -16,7 +15,7 @@ export type SubjectChain = [any, ...QueryFunction[]] export interface StateFunc { (): Record (v: Record): Record - (k: 'activeSessions', v?: Cypress.Commands.Session.ActiveSessions): Cypress.Commands.Session.ActiveSessions | undefined + (k: 'activeSessions', v?: Cypress.ActiveSessions): Cypress.ActiveSessions | undefined (k: '$autIframe', v?: JQuery): JQuery | undefined (k: 'routes', v?: RouteMap): RouteMap (k: 'aliasedRequests', v?: AliasedRequest[]): AliasedRequest[] diff --git a/packages/driver/types/cy/commands/session.d.ts b/packages/driver/types/cy/commands/session.d.ts deleted file mode 100644 index fb5c7d865053..000000000000 --- a/packages/driver/types/cy/commands/session.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -// The type declarations for Cypress Log Group & the corresponding configuration permutations -declare namespace Cypress { - declare namespace Commands { - declare namespace Session { - type ActiveSessions = Record - type SessionSetup = (log: Cypress.Log) => Chainable - type SessionValidation = (log: Cypress.Log) => Chainable - - interface Storage { - origin: string - value: Record - } - - interface SessionData { - id: string - cacheAcrossSpecs: boolean - cookies?: Array | null - localStorage?: Array | null - sessionStorage?: Array | null - setup: () => void - hydrated: boolean - validate?: Cypress.SessionOptions['validate'] - } - } - } -} diff --git a/packages/driver/types/internal-types-lite.d.ts b/packages/driver/types/internal-types-lite.d.ts index 3c2d936292be..5f0aed8797f5 100644 --- a/packages/driver/types/internal-types-lite.d.ts +++ b/packages/driver/types/internal-types-lite.d.ts @@ -1,4 +1,3 @@ -/// /// /// @@ -32,11 +31,11 @@ declare namespace Cypress { // We should decide whether calling with id is correct or not. clearTimeout: ITimeouts['clearTimeout'] isStable: IStability['isStable'] - fail: (err: Error, options:{ async?: boolean }) => Error + fail: (err: Error, options: { async?: boolean }) => Error getRemoteLocation: ILocation['getRemoteLocation'] subjectChain: (chainerId?: string) => SubjectChain - createSnapshot: ISnapshots['createSnapshot'] + createSnapshot: ISnapshots['createSnapshot'] getStyles: ISnapshots['getStyles'] } } diff --git a/packages/server/lib/session.ts b/packages/server/lib/session.ts index edeb97df1112..aeb5a67b4ef4 100644 --- a/packages/server/lib/session.ts +++ b/packages/server/lib/session.ts @@ -1,4 +1,4 @@ -import type { ServerSessionData, StoredSessions } from '@packages/types' +import type { StoredSessions } from '@packages/types' type State = { globalSessions: StoredSessions @@ -10,7 +10,7 @@ const state: State = { specSessions: {}, } -export function saveSession (data: ServerSessionData): void { +export function saveSession (data: Cypress.ServerSessionData): void { if (!data.id) throw new Error('session data had no id') if (data.cacheAcrossSpecs) { @@ -26,7 +26,7 @@ export function getActiveSessions (): StoredSessions { return state.globalSessions } -export function getSession (id: string): ServerSessionData { +export function getSession (id: string): Cypress.ServerSessionData { const session = state.globalSessions[id] || state.specSessions[id] if (!session) throw new Error(`session with id "${id}" not found`) diff --git a/packages/types/src/driver.ts b/packages/types/src/driver.ts index 8392302421dc..58bdb4fa21a5 100644 --- a/packages/types/src/driver.ts +++ b/packages/types/src/driver.ts @@ -21,21 +21,7 @@ export interface Emissions { ended: Record } -interface HtmlWebStorage { - origin: string - value: Record -} - -export interface ServerSessionData { - id: string - cacheAcrossSpecs: boolean - cookies: Cypress.Cookie[] | null - localStorage: Array | null - sessionStorage: Array | null - setup: string -} - -export type StoredSessions = Record +export type StoredSessions = Record export interface CachedTestState { activeSessions: StoredSessions diff --git a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json index 0ae4c87680fe..34294b926b09 100644 --- a/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json +++ b/tooling/v8-snapshot/cache/dev-darwin/snapshot-meta.cache.json @@ -3542,5 +3542,5 @@ "./tooling/v8-snapshot/cache/dev-darwin/snapshot-entry.js" ], "deferredHashFile": "yarn.lock", - "deferredHash": "844da7908a41692a3b04716c88e2f0cdad85ece6f94f6ab89fbd1ffe5c332fd2" + "deferredHash": "e48c44c628ea6b968dae0697bf1fb37e18bc69913676524512ca41dcff3a985a" } \ No newline at end of file