mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-30 12:01:06 -05:00
feat: cache sessions between specs
This commit is contained in:
@@ -104,9 +104,11 @@ const configChangeHandler: SubscriptionHandlerArg<any, any> = (
|
||||
window.__CYPRESS_CONFIG__ = next.configChange.serveConfig
|
||||
|
||||
const eventManager = useEventManager()
|
||||
const isRerun = true
|
||||
|
||||
eventManager.runSpec()
|
||||
eventManager.runSpec(isRerun)
|
||||
} catch (e) {
|
||||
console.warn(e)
|
||||
// eventManager may not be defined, for example if the spec
|
||||
// is still loading.
|
||||
// In that case, just do nothing - the spec will be executed soon.
|
||||
|
||||
@@ -274,6 +274,7 @@ function openFile () {
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const eventManager = getEventManager()
|
||||
|
||||
|
||||
@@ -35,12 +35,14 @@ export class AutIframe {
|
||||
return $iframe
|
||||
}
|
||||
|
||||
showInitialBlankContentsE2E () {
|
||||
this._showContents(blankContents.initial())
|
||||
}
|
||||
showInitialBlankContents (testingType) {
|
||||
if (testingType === 'component') {
|
||||
this._showContents(blankContents.initialCT())
|
||||
|
||||
showInitialBlankContentsCT () {
|
||||
this._showContents(blankContents.initialCT())
|
||||
return
|
||||
}
|
||||
|
||||
this._showContents(blankContents.initial())
|
||||
}
|
||||
|
||||
showSessionBlankContents () {
|
||||
|
||||
@@ -85,12 +85,11 @@ export class EventManager {
|
||||
|
||||
const rerun = () => {
|
||||
if (!this) {
|
||||
// if the tests have been reloaded
|
||||
// then there is nothing to rerun
|
||||
// if the tests have been reloaded then there is nothing to rerun
|
||||
return
|
||||
}
|
||||
|
||||
return this.runSpec(state)
|
||||
return this.rerunSpec()
|
||||
}
|
||||
|
||||
const connectionInfo: AddGlobalListenerOptions = {
|
||||
@@ -251,10 +250,10 @@ export class EventManager {
|
||||
this.saveState(state)
|
||||
})
|
||||
|
||||
this.reporterBus.on('clear:session', () => {
|
||||
this.reporterBus.on('clear:all:sessions', () => {
|
||||
if (!Cypress) return
|
||||
|
||||
Cypress.backend('clear:session')
|
||||
Cypress.backend('clear:all:sessions')
|
||||
.then(() => {
|
||||
rerun()
|
||||
})
|
||||
@@ -341,7 +340,7 @@ export class EventManager {
|
||||
const $window = this.$CypressDriver.$(window)
|
||||
|
||||
// This is a test-only even. It's used to
|
||||
// trigger a re-reun for the drive rerun.cy.js spec.
|
||||
// trigger a re-run for the drive rerun.cy.js spec.
|
||||
$window.on('test:trigger:rerun', rerun)
|
||||
|
||||
// when we actually unload then
|
||||
@@ -372,7 +371,13 @@ export class EventManager {
|
||||
}
|
||||
|
||||
setup (config) {
|
||||
Cypress = this.Cypress = this.$CypressDriver.create(config)
|
||||
const cachedState = new Promise((resolve) => {
|
||||
this.ws.emit('get:cached:state', (cachedState = {}) => {
|
||||
resolve(cachedState)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress = this.Cypress = this.$CypressDriver.create(config, cachedState)
|
||||
|
||||
// expose Cypress globally
|
||||
// @ts-ignore
|
||||
@@ -380,6 +385,9 @@ export class EventManager {
|
||||
|
||||
this._addListeners()
|
||||
|
||||
// console.log('clear spec sessions')
|
||||
// Cypress.backend('clear:spec:sessions')
|
||||
|
||||
this.ws.emit('watch:test:file', config.spec)
|
||||
}
|
||||
|
||||
@@ -600,6 +608,7 @@ export class EventManager {
|
||||
|
||||
// Inform all spec bridges that the primary origin has begun to unload.
|
||||
Cypress.on('window:before:unload', () => {
|
||||
console.log('window:before:unload')
|
||||
Cypress.primaryOriginCommunicator.toAllSpecBridges('before:unload')
|
||||
})
|
||||
|
||||
@@ -719,13 +728,22 @@ export class EventManager {
|
||||
this.ws.off()
|
||||
}
|
||||
|
||||
async teardown (state: MobxRunnerStore) {
|
||||
async teardown (state, isRerun = false) {
|
||||
console.log('teardown', isRerun, !Cypress)
|
||||
if (!Cypress) {
|
||||
return
|
||||
}
|
||||
|
||||
state.setIsLoading(true)
|
||||
|
||||
if (!isRerun) {
|
||||
console.log('teardown--clear:spec:sessions')
|
||||
// only clear spec sessions when a new spec is selected
|
||||
Cypress.backend('clear:spec:sessions')
|
||||
}
|
||||
|
||||
console.log('stop')
|
||||
|
||||
// when we are re-running we first
|
||||
// need to stop cypress always
|
||||
Cypress.stop()
|
||||
@@ -744,26 +762,19 @@ export class EventManager {
|
||||
})
|
||||
}
|
||||
|
||||
async _rerun () {
|
||||
await this.resetReporter()
|
||||
|
||||
// this probably isn't 100% necessary
|
||||
// since Cypress will fall out of scope
|
||||
// but we want to be aggressive here
|
||||
// and force GC early and often
|
||||
Cypress.removeAllListeners()
|
||||
|
||||
this.localBus.emit('restart')
|
||||
}
|
||||
|
||||
async runSpec (state: MobxRunnerStore) {
|
||||
if (!Cypress) {
|
||||
async rerunSpec () {
|
||||
if (!this || !Cypress) {
|
||||
// if the tests have been reloaded then there is nothing to rerun
|
||||
return
|
||||
}
|
||||
|
||||
await this.teardown(state)
|
||||
await this.resetReporter()
|
||||
|
||||
return this._rerun()
|
||||
// this probably isn't 100% necessary since Cypress will fall out of scope
|
||||
// but we want to be aggressive here and force GC early and often
|
||||
Cypress.removeAllListeners()
|
||||
|
||||
this.localBus.emit('restart')
|
||||
}
|
||||
|
||||
_interceptStudio (displayProps) {
|
||||
|
||||
@@ -165,10 +165,11 @@ function getSpecUrl (namespace: string, specSrc: string) {
|
||||
* This should be called before you execute a spec,
|
||||
* or re-running the current spec.
|
||||
*/
|
||||
function teardownSpec () {
|
||||
function teardownSpec (isRerun = false) {
|
||||
console.log('teardown spec')
|
||||
useSnapshotStore().$reset()
|
||||
|
||||
return getEventManager().teardown(getMobxRunnerStore())
|
||||
return getEventManager().teardown(getMobxRunnerStore(), isRerun)
|
||||
}
|
||||
|
||||
let isTorndown = false
|
||||
@@ -210,12 +211,7 @@ export function addCrossOriginIframe (location) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a spec by creating a fresh AUT and initializing
|
||||
* Cypress on it.
|
||||
*
|
||||
*/
|
||||
function runSpecCT (spec: SpecFile) {
|
||||
function setupDriver (spec: SpecFile) {
|
||||
// TODO: UNIFY-1318 - figure out how to manage window.config.
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
@@ -241,11 +237,31 @@ function runSpecCT (spec: SpecFile) {
|
||||
|
||||
// create new AUT
|
||||
const autIframe = getAutIframeModel()
|
||||
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
|
||||
return {
|
||||
autIframe,
|
||||
config,
|
||||
$container,
|
||||
$autIframe,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a spec by creating a fresh AUT and initializing
|
||||
* Cypress on it.
|
||||
*
|
||||
*/
|
||||
function runSpecCT (spec: SpecFile) {
|
||||
const {
|
||||
autIframe,
|
||||
config,
|
||||
$autIframe,
|
||||
} = setupDriver(spec)
|
||||
const specSrc = getSpecUrl(config.namespace, spec.absolute)
|
||||
|
||||
autIframe.showInitialBlankContentsCT()
|
||||
autIframe.showInitialBlankContents('component')
|
||||
$autIframe.prop('src', specSrc)
|
||||
|
||||
// initialize Cypress (driver) with the AUT!
|
||||
@@ -281,40 +297,19 @@ function setSpecForDriver (spec: SpecFile) {
|
||||
* initialize Cypress on the AUT.
|
||||
*/
|
||||
function runSpecE2E (spec: SpecFile) {
|
||||
// TODO: UNIFY-1318 - manage config with GraphQL, don't put it on window.
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
// this is how the Cypress driver knows which spec to run.
|
||||
config.spec = setSpecForDriver(spec)
|
||||
|
||||
// creates a new instance of the Cypress driver for this spec,
|
||||
// initializes a bunch of listeners
|
||||
// watches spec file for changes.
|
||||
getEventManager().setup(config)
|
||||
|
||||
const $runnerRoot = getRunnerElement()
|
||||
|
||||
// clear AUT, if there is one.
|
||||
empty($runnerRoot)
|
||||
|
||||
// create root for new AUT
|
||||
const $container = document.createElement('div')
|
||||
|
||||
$container.classList.add('screenshot-height-container')
|
||||
|
||||
$runnerRoot.append($container)
|
||||
|
||||
// create new AUT
|
||||
const autIframe = getAutIframeModel()
|
||||
|
||||
const $autIframe: JQuery<HTMLIFrameElement> = autIframe.create().appendTo($container)
|
||||
const {
|
||||
autIframe,
|
||||
config,
|
||||
$autIframe,
|
||||
$container,
|
||||
} = setupDriver(spec)
|
||||
|
||||
// Remove the spec bridge iframe
|
||||
document.querySelectorAll('iframe.spec-bridge-iframe').forEach((el) => {
|
||||
el.remove()
|
||||
})
|
||||
|
||||
autIframe.showInitialBlankContentsE2E()
|
||||
autIframe.showInitialBlankContents('e2e')
|
||||
|
||||
// create Spec IFrame
|
||||
const specSrc = getSpecUrl(config.namespace, encodeURIComponent(spec.relative))
|
||||
@@ -399,8 +394,9 @@ async function initialize () {
|
||||
* 5. Setup the spec. This involves a few things, see the `runSpecCT` function's
|
||||
* description for more information.
|
||||
*/
|
||||
async function executeSpec (spec: SpecFile) {
|
||||
await teardownSpec()
|
||||
async function executeSpec (spec: SpecFile, isRerun = false) {
|
||||
console.log('execute spec', isRerun)
|
||||
await teardownSpec(isRerun)
|
||||
|
||||
const mobxRunnerStore = getMobxRunnerStore()
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@ export function useEventManager () {
|
||||
const autStore = useAutStore()
|
||||
const specStore = useSpecStore()
|
||||
|
||||
function runSpec () {
|
||||
function runSpec (isRerun = false) {
|
||||
if (!specStore.activeSpec) {
|
||||
throw Error(`Cannot run spec when specStore.active spec is null or undefined!`)
|
||||
}
|
||||
|
||||
autStore.setScriptError(null)
|
||||
UnifiedRunnerAPI.executeSpec(specStore.activeSpec)
|
||||
UnifiedRunnerAPI.executeSpec(specStore.activeSpec, isRerun)
|
||||
}
|
||||
|
||||
function initializeRunnerLifecycleEvents () {
|
||||
@@ -23,7 +23,9 @@ export function useEventManager () {
|
||||
eventManager.on('restart', () => {
|
||||
// If we get the event to restart but have already navigated away from the runner, don't execute the spec
|
||||
if (specStore.activeSpec) {
|
||||
runSpec()
|
||||
const isRerun = true
|
||||
|
||||
runSpec(isRerun)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,6 +46,7 @@ export function useEventManager () {
|
||||
|
||||
const startSpecWatcher = () => {
|
||||
return watch(() => specStore.activeSpec, () => {
|
||||
console.log('startSpecWatcher')
|
||||
if (specStore.activeSpec) {
|
||||
runSpec()
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ export default defineConfig({
|
||||
return require('./cypress/plugins')(on, config)
|
||||
},
|
||||
'baseUrl': 'http://localhost:3500',
|
||||
experimentalSessionAndOrigin: true,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
it.only('s1 - t1', () => {
|
||||
cy.session('session1', () => {
|
||||
// do something
|
||||
}, {
|
||||
cacheAcrossSpecs: true,
|
||||
})
|
||||
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
|
||||
it('s1- t2', () => {
|
||||
cy.session('session2', () => {
|
||||
// do something
|
||||
// something else
|
||||
}, {
|
||||
})
|
||||
|
||||
cy.visit('https://example.cypress.io')
|
||||
cy.visit('https://example.cypress.io')
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
@@ -0,0 +1,19 @@
|
||||
it.only('s2 - t1', () => {
|
||||
cy.session('session1', () => {
|
||||
// do something
|
||||
}, {
|
||||
cacheAcrossSpecs: true,
|
||||
})
|
||||
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
|
||||
it('s2 -t2', () => {
|
||||
cy.session('session2', () => {
|
||||
// do something
|
||||
// something else
|
||||
}, {
|
||||
})
|
||||
|
||||
cy.visit('https://example.cypress.io')
|
||||
})
|
||||
@@ -245,7 +245,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
})
|
||||
|
||||
it('sessions.clearAllSavedSessions()', async () => {
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:session').resolves(null)
|
||||
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('clear:all:sessions').resolves(null)
|
||||
|
||||
const sessionsManager = new SessionsManager(CypressInstance, () => {})
|
||||
const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions')
|
||||
@@ -253,7 +253,7 @@ describe('src/cy/commands/sessions/manager.ts', () => {
|
||||
await sessionsManager.sessions.clearAllSavedSessions()
|
||||
|
||||
expect(sessionsSpy).to.be.calledOnce
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:session', null)
|
||||
expect(cypressSpy).to.be.calledOnceWith('clear:all:sessions', null)
|
||||
})
|
||||
|
||||
describe('.clearCurrentSessionData()', () => {
|
||||
|
||||
@@ -1,23 +1,141 @@
|
||||
import { curry } from 'lodash'
|
||||
import { findCrossOriginLogs } from '../../../../support/utils'
|
||||
|
||||
context('cy.origin local storage', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
})
|
||||
const session = () => {
|
||||
return cy.session('session', () => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('foo', 'bar')
|
||||
document.cookie = 'favorite_food=milkshake; SameSite=None; Secure'
|
||||
expect(win.localStorage.getItem('foo')).to.equal('bar')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
it.only('.clearLocalStorage()', () => {
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
session()
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.getItem('foo')).to.equal(null)
|
||||
})
|
||||
|
||||
cy.getCookies().should('have.length', 0)
|
||||
|
||||
cy.then(async () => {
|
||||
const currOriginStorage = await Cypress.session.getStorage()
|
||||
|
||||
expect(currOriginStorage.localStorage).to.have.length(0)
|
||||
|
||||
console.log('currOriginStorage', currOriginStorage)
|
||||
|
||||
const allOriginStorage = await Cypress.session.getStorage({ origin: '*' })
|
||||
|
||||
expect(allOriginStorage.localStorage).to.have.length(1)
|
||||
expect(allOriginStorage.localStorage[0].value).to.deep.eq({ foo: 'bar' })
|
||||
|
||||
console.log('allOriginStorage', allOriginStorage)
|
||||
})
|
||||
|
||||
cy.log('clear all local storage')
|
||||
cy.then(async () => {
|
||||
await Cypress.session.clearStorage()
|
||||
|
||||
const currOriginStorage = await Cypress.session.getStorage()
|
||||
|
||||
expect(currOriginStorage.localStorage).to.have.length(0)
|
||||
|
||||
console.log('currOriginStorage', currOriginStorage)
|
||||
|
||||
const allOriginStorage = await Cypress.session.getStorage({ origin: '*' })
|
||||
|
||||
expect(allOriginStorage.localStorage).to.have.length(0)
|
||||
|
||||
console.log('allOriginStorage', allOriginStorage)
|
||||
})
|
||||
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('foo', 'bar')
|
||||
expect(win.localStorage.getItem('foo')).to.equal(null)
|
||||
})
|
||||
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
|
||||
it('.clearLocalStorage()', () => {
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
win.localStorage.setItem('foo', 'bar')
|
||||
expect(win.localStorage.getItem('foo')).to.equal('bar')
|
||||
})
|
||||
|
||||
cy.clearLocalStorage().should((localStorage) => {
|
||||
expect(localStorage.length).to.equal(0)
|
||||
expect(localStorage.getItem('foo')).to.be.null
|
||||
// // cy.clearLocalStorage().should((localStorage) => {
|
||||
// // expect(localStorage.length).to.equal(0)
|
||||
// // expect(localStorage.getItem('foo')).to.be.null
|
||||
// // })
|
||||
})
|
||||
})
|
||||
|
||||
it.only('.clearLocalStorage()', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.length).to.equal(0)
|
||||
})
|
||||
|
||||
cy.pause()
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.length).to.equal(0)
|
||||
})
|
||||
})
|
||||
|
||||
session()
|
||||
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.getItem('foo')).to.equal(null)
|
||||
})
|
||||
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.getItem('foo')).to.equal('bar')
|
||||
})
|
||||
})
|
||||
|
||||
cy.then(async () => {
|
||||
const currOriginStorage = await Cypress.session.getStorage()
|
||||
|
||||
expect(currOriginStorage.localStorage).to.have.length(0)
|
||||
|
||||
console.log('currOriginStorage', currOriginStorage)
|
||||
|
||||
const allOriginStorage = await Cypress.session.getStorage({ origin: '*' })
|
||||
|
||||
expect(allOriginStorage.localStorage).to.have.length(1)
|
||||
expect(allOriginStorage.localStorage[0].value).to.deep.eq({ foo: 'bar' })
|
||||
|
||||
console.log('allOriginStorage', allOriginStorage)
|
||||
})
|
||||
|
||||
// Cypress.session.clearAllSavedSessions()
|
||||
cy.visit('/fixtures/primary-origin.html')
|
||||
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
|
||||
|
||||
cy.origin('http://foobar.com:3500', () => {
|
||||
cy.window().then((win) => {
|
||||
expect(win.localStorage.getItem('foo')).to.equal(null)
|
||||
})
|
||||
|
||||
// // cy.clearLocalStorage().should((localStorage) => {
|
||||
// // expect(localStorage.length).to.equal(0)
|
||||
// // expect(localStorage.getItem('foo')).to.be.null
|
||||
// // })
|
||||
})
|
||||
})
|
||||
|
||||
context('#consoleProps', () => {
|
||||
|
||||
@@ -12,6 +12,11 @@ import {
|
||||
|
||||
type SessionData = Cypress.Commands.Session.SessionData
|
||||
|
||||
type SessionOptions = {
|
||||
cacheAcrossSpecs: boolean
|
||||
validate?: Function
|
||||
}
|
||||
|
||||
/**
|
||||
* Session data should be cleared with spec browser launch.
|
||||
*
|
||||
@@ -52,7 +57,8 @@ export default function (Commands, Cypress, cy) {
|
||||
})
|
||||
|
||||
Commands.addAll({
|
||||
session (id, setup?: Function, options: { validate?: Function } = {}) {
|
||||
session (id, setup?: Function, options: SessionOptions = { cacheAcrossSpecs: false }) {
|
||||
console.log('session command', Cypress.state('activeSessions'))
|
||||
throwIfNoSessionSupport()
|
||||
|
||||
if (!id || !_.isString(id) && !_.isObject(id)) {
|
||||
@@ -72,6 +78,7 @@ export default function (Commands, Cypress, cy) {
|
||||
|
||||
const validOpts = {
|
||||
'validate': 'function',
|
||||
'cacheAcrossSpecs': 'boolean',
|
||||
}
|
||||
|
||||
Object.entries(options).forEach(([key, value]) => {
|
||||
@@ -108,6 +115,7 @@ export default function (Commands, Cypress, cy) {
|
||||
id,
|
||||
setup,
|
||||
validate: options.validate,
|
||||
cacheAcrossSpecs: options.cacheAcrossSpecs,
|
||||
})
|
||||
|
||||
sessionsManager.currentTestRegisteredSessions.set(id, true)
|
||||
@@ -355,6 +363,7 @@ export default function (Commands, Cypress, cy) {
|
||||
if (!existingSession.hydrated) {
|
||||
const serverStoredSession = await sessions.getSession(existingSession.id).catch(_.noop)
|
||||
|
||||
console.log('serverStoredSession', serverStoredSession)
|
||||
// we have a saved session on the server and setup matches
|
||||
if (serverStoredSession && serverStoredSession.setup === existingSession.setup.toString()) {
|
||||
_.extend(existingSession, _.omit(serverStoredSession, 'setup'))
|
||||
|
||||
@@ -123,6 +123,7 @@ export default class SessionsManager {
|
||||
setup: options.setup,
|
||||
hydrated: false,
|
||||
validate: options.validate,
|
||||
cacheAcrossSpecs: options.cacheAcrossSpecs,
|
||||
}
|
||||
|
||||
this.setActiveSession({ [sess_state.id]: sess_state })
|
||||
@@ -133,7 +134,13 @@ export default class SessionsManager {
|
||||
clearAllSavedSessions: async () => {
|
||||
this.clearActiveSessions()
|
||||
|
||||
return this.Cypress.backend('clear:session', null)
|
||||
return this.Cypress.backend('clear:all:sessions', null)
|
||||
},
|
||||
|
||||
clearAllSpecSessions: async () => {
|
||||
this.clearActiveSessions()
|
||||
|
||||
return this.Cypress.backend('clear:spec:sessions', null)
|
||||
},
|
||||
|
||||
clearCurrentSessionData: async () => {
|
||||
|
||||
@@ -44,6 +44,41 @@ const getCurrentOriginStorage = () => {
|
||||
return value
|
||||
}
|
||||
|
||||
const getConsoleProps = (sessState: SessionData) => {
|
||||
const sessionDetails = getSessionDetailsForTable(sessState)
|
||||
|
||||
const tables = _.flatMap(sessionDetails, (val, domain) => {
|
||||
const cookiesTable = () => {
|
||||
return {
|
||||
name: `🍪 Cookies - ${domain} (${val.cookies.length})`,
|
||||
data: val.cookies,
|
||||
}
|
||||
}
|
||||
|
||||
const localStorageTable = () => {
|
||||
return {
|
||||
name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`,
|
||||
data: _.map(val.localStorage.value, (value, key) => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
val.cookies && cookiesTable,
|
||||
val.localStorage && localStorageTable,
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
id: sessState.id,
|
||||
table: _.compact(tables),
|
||||
}
|
||||
}
|
||||
|
||||
const setPostMessageLocalStorage = async (specWindow, originOptions) => {
|
||||
const origins = originOptions.map((v) => v.origin) as string[]
|
||||
|
||||
@@ -96,7 +131,7 @@ const setPostMessageLocalStorage = async (specWindow, originOptions) => {
|
||||
.timeout(2000)
|
||||
.finally(() => {
|
||||
specWindow.removeEventListener('message', onPostMessage)
|
||||
$iframeContainer.remove()
|
||||
// $iframeContainer.remove()
|
||||
})
|
||||
.catch(() => {
|
||||
Cypress.log({
|
||||
@@ -106,48 +141,13 @@ const setPostMessageLocalStorage = async (specWindow, originOptions) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getConsoleProps = (sessState: SessionData) => {
|
||||
const sessionDetails = getSessionDetailsForTable(sessState)
|
||||
|
||||
const tables = _.flatMap(sessionDetails, (val, domain) => {
|
||||
const cookiesTable = () => {
|
||||
return {
|
||||
name: `🍪 Cookies - ${domain} (${val.cookies.length})`,
|
||||
data: val.cookies,
|
||||
}
|
||||
}
|
||||
|
||||
const localStorageTable = () => {
|
||||
return {
|
||||
name: `📁 Storage - ${domain} (${_.keys(val.localStorage.value).length})`,
|
||||
data: _.map(val.localStorage.value, (value, key) => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
val.cookies && cookiesTable,
|
||||
val.localStorage && localStorageTable,
|
||||
]
|
||||
})
|
||||
|
||||
return {
|
||||
id: sessState.id,
|
||||
table: _.compact(tables),
|
||||
}
|
||||
}
|
||||
|
||||
const getPostMessageLocalStorage = (specWindow, origins): Promise<any[]> => {
|
||||
const results = [] as any[]
|
||||
const iframes: JQuery<HTMLElement>[] = []
|
||||
let onPostMessage
|
||||
const successOrigins = [] as string[]
|
||||
|
||||
const $iframeContainer = $(`<div style="display:none"></div>`).appendTo($('body', specWindow.document))
|
||||
const $iframeContainer = $(`<div id="sessions-iframe-container" style="display:none"></div>`).appendTo($('body', specWindow.document))
|
||||
|
||||
_.each(origins, (u) => {
|
||||
const $iframe = $(`<iframe src="${`${u}/__cypress/automation/getLocalStorage`}"></iframe>`)
|
||||
@@ -180,7 +180,7 @@ const getPostMessageLocalStorage = (specWindow, origins): Promise<any[]> => {
|
||||
.timeout(2000)
|
||||
.finally(() => {
|
||||
specWindow.removeEventListener('message', onPostMessage)
|
||||
$iframeContainer.remove()
|
||||
// $iframeContainer.remove()
|
||||
})
|
||||
.catch((err) => {
|
||||
Cypress.log({
|
||||
|
||||
@@ -169,7 +169,7 @@ class $Cypress {
|
||||
_.extend(this.$, $)
|
||||
}
|
||||
|
||||
configure (config: Cypress.ObjectLike = {}) {
|
||||
configure (config: Record<string, any> = {}, cachedState: Promise<Record<string, any>>) {
|
||||
const domainName = config.remote ? config.remote.domainName : undefined
|
||||
|
||||
// set domainName but allow us to turn
|
||||
@@ -216,8 +216,25 @@ class $Cypress {
|
||||
config = _.omit(config, 'env', 'remote', 'resolved', 'scaffoldedFiles', 'state', 'testingType', 'isCrossOriginSpecBridge')
|
||||
|
||||
_.extend(this, browserInfo(config))
|
||||
// cachedState.then((cache) => {
|
||||
// const cache = this.emit('get:cached:state', (cache) => {
|
||||
// console.log('in cypress', cache)
|
||||
|
||||
this.state = $SetterGetter.create({}) as unknown as StateFunc
|
||||
// return {
|
||||
// activeSessions: cache.globalSessions,
|
||||
// }
|
||||
// })
|
||||
|
||||
// console.log('cache in cy', window.getEventNames())
|
||||
this.state = $SetterGetter.create({
|
||||
...cachedState.then((state) => {
|
||||
console.log(state.globalSessions)
|
||||
|
||||
return {
|
||||
activeSessions: state.globalSessions || {},
|
||||
}
|
||||
}),
|
||||
}) as unknown as StateFunc
|
||||
|
||||
/*
|
||||
* As part of the Detached DOM effort, we're changing the way subjects are determined in Cypress.
|
||||
@@ -312,6 +329,7 @@ class $Cypress {
|
||||
// Method to manually re-execute Runner (usually within $autIframe)
|
||||
// used mainly by Component Testing
|
||||
restartRunner () {
|
||||
console.log('[cypres] restartrunner')
|
||||
if (!window.top!.Cypress) {
|
||||
throw Error('Cannot re-run spec without Cypress')
|
||||
}
|
||||
@@ -378,7 +396,7 @@ class $Cypress {
|
||||
})
|
||||
.then(() => {
|
||||
this.cy.initialize(this.$autIframe)
|
||||
|
||||
console.log(this.state())
|
||||
this.onSpecReady()
|
||||
})
|
||||
}
|
||||
@@ -815,10 +833,10 @@ class $Cypress {
|
||||
}
|
||||
}
|
||||
|
||||
static create (config) {
|
||||
static create (config: Record<string, any>, cachedState: Promise<Record<string, any>>) {
|
||||
const cypress = new $Cypress()
|
||||
|
||||
cypress.configure(config)
|
||||
cypress.configure(config, cachedState)
|
||||
|
||||
return cypress
|
||||
}
|
||||
|
||||
@@ -642,6 +642,7 @@ export class $Cy extends EventEmitter2 implements ITimeouts, IStability, IAssert
|
||||
try {
|
||||
const s = this.state()
|
||||
|
||||
console.log('reset', test.title, s.activeSessions)
|
||||
const backup = {
|
||||
window: s.window,
|
||||
document: s.document,
|
||||
|
||||
@@ -13,6 +13,7 @@ declare namespace Cypress {
|
||||
|
||||
interface SessionData {
|
||||
id: string
|
||||
cacheAcrossSpecs: boolean
|
||||
cookies?: Array<Cypress.Cookie> | null
|
||||
localStorage?: Array<LocalStorage> | null
|
||||
setup: () => void
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
### This file was generated by Nexus Schema
|
||||
### Do not make changes to this file directly
|
||||
|
||||
"""
|
||||
Feature not available for subscription
|
||||
"""
|
||||
type CloudFeatureNotEnabled {
|
||||
"""
|
||||
an error message
|
||||
"""
|
||||
message: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a pollable status for clients to know when refetching data is required.
|
||||
"""
|
||||
@@ -211,6 +221,17 @@ type CloudProjectSpec implements Node {
|
||||
"""
|
||||
fromBranch: String!
|
||||
): Float
|
||||
flakyStatus(
|
||||
"""
|
||||
The number of runs to consider when counting flaky runs.
|
||||
"""
|
||||
flakyRunsWindow: Int!
|
||||
|
||||
"""
|
||||
The branch to measure average duration against. This will fallback to the closest branch with data.
|
||||
"""
|
||||
fromBranch: String!
|
||||
): CloudProjectSpecFlakyResult
|
||||
|
||||
"""
|
||||
Globally unique identifier representing a concrete GraphQL ObjectType
|
||||
@@ -264,6 +285,29 @@ type CloudProjectSpec implements Node {
|
||||
): CloudSpecRunConnection
|
||||
}
|
||||
|
||||
union CloudProjectSpecFlakyResult =
|
||||
CloudFeatureNotEnabled
|
||||
| CloudProjectSpecFlakyStatus
|
||||
|
||||
type CloudProjectSpecFlakyStatus {
|
||||
"""
|
||||
URL linking to the flaky data in the Cypress dashboard for this spec
|
||||
"""
|
||||
dashboardUrl: String
|
||||
|
||||
"""
|
||||
Number of flaky runs from the considered runs
|
||||
"""
|
||||
flakyRuns: Int
|
||||
flakyRunsWindow: Int
|
||||
|
||||
"""
|
||||
The last flaky run occurrence, interpreted as "n runs ago" - ex: a value of 5 means a flaky run last occurred 5 runs ago
|
||||
"""
|
||||
lastFlaky: Int
|
||||
severity: String
|
||||
}
|
||||
|
||||
"""
|
||||
Unable to find cloud spec in project
|
||||
"""
|
||||
|
||||
@@ -65,6 +65,12 @@ type CachedUser implements Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
"""Feature not available for subscription"""
|
||||
type CloudFeatureNotEnabled {
|
||||
"""an error message"""
|
||||
message: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a pollable status for clients to know when refetching data is required.
|
||||
"""
|
||||
@@ -208,6 +214,15 @@ type CloudProjectSpec implements Node {
|
||||
"""
|
||||
fromBranch: String!
|
||||
): Float
|
||||
flakyStatus(
|
||||
"""The number of runs to consider when counting flaky runs."""
|
||||
flakyRunsWindow: Int!
|
||||
|
||||
"""
|
||||
The branch to measure average duration against. This will fallback to the closest branch with data.
|
||||
"""
|
||||
fromBranch: String!
|
||||
): CloudProjectSpecFlakyResult
|
||||
|
||||
"""Globally unique identifier representing a concrete GraphQL ObjectType"""
|
||||
id: ID!
|
||||
@@ -245,6 +260,23 @@ type CloudProjectSpec implements Node {
|
||||
): CloudSpecRunConnection
|
||||
}
|
||||
|
||||
union CloudProjectSpecFlakyResult = CloudFeatureNotEnabled | CloudProjectSpecFlakyStatus
|
||||
|
||||
type CloudProjectSpecFlakyStatus {
|
||||
"""URL linking to the flaky data in the Cypress dashboard for this spec"""
|
||||
dashboardUrl: String
|
||||
|
||||
"""Number of flaky runs from the considered runs"""
|
||||
flakyRuns: Int
|
||||
flakyRunsWindow: Int
|
||||
|
||||
"""
|
||||
The last flaky run occurrence, interpreted as "n runs ago" - ex: a value of 5 means a flaky run last occurred 5 runs ago
|
||||
"""
|
||||
lastFlaky: Int
|
||||
severity: String
|
||||
}
|
||||
|
||||
"""Unable to find cloud spec in project"""
|
||||
type CloudProjectSpecNotFound {
|
||||
"""an error message"""
|
||||
|
||||
@@ -312,6 +312,8 @@ export class Http {
|
||||
}
|
||||
|
||||
getRenderedHTMLOrigins = () => {
|
||||
console.log('renderedHTMLOrigins', this.renderedHTMLOrigins)
|
||||
|
||||
return this.renderedHTMLOrigins
|
||||
}
|
||||
|
||||
|
||||
@@ -192,8 +192,8 @@ const events: Events = {
|
||||
runner.emit('get:user:editor', cb)
|
||||
})
|
||||
|
||||
localBus.on('clear:session', (cb) => {
|
||||
runner.emit('clear:session', cb)
|
||||
localBus.on('clear:all:sessions', (cb) => {
|
||||
runner.emit('clear:all:sessions', cb)
|
||||
})
|
||||
|
||||
localBus.on('set:user:editor', (editor) => {
|
||||
|
||||
@@ -46,7 +46,7 @@ class Sessions extends React.Component<SessionsProps> {
|
||||
headerClass='hook-header'
|
||||
headerExtras={
|
||||
<div className="clear-sessions"
|
||||
onClick={() => events.emit('clear:session')}
|
||||
onClick={() => events.emit('clear:all:sessions')}
|
||||
><span><i className="fas fa-ban" /> Clear All Sessions</span></div>}
|
||||
contentClass='instrument-content'
|
||||
>
|
||||
|
||||
@@ -169,7 +169,7 @@ export class OpenProject {
|
||||
|
||||
// clear cookies and all session data before each spec
|
||||
cookieJar.removeAllCookies()
|
||||
session.clearSessions()
|
||||
session.clearSpecSessions()
|
||||
})
|
||||
.then(() => {
|
||||
// TODO: Stub this so we can detect it being called
|
||||
|
||||
@@ -69,8 +69,7 @@ const API = {
|
||||
// vs config.isInterativeMode
|
||||
const shouldWatch = !config.isTextTerminal || Boolean(process.env.CYPRESS_INTERNAL_FORCE_FILEWATCH)
|
||||
|
||||
const baseFilePath = filePath
|
||||
.replace(config.projectRoot, '')
|
||||
const baseFilePath = filePath.replace(config.projectRoot, '')
|
||||
|
||||
fileObject = (fileObjects[filePath] = _.extend(new EE(), {
|
||||
filePath,
|
||||
|
||||
@@ -1,23 +1,44 @@
|
||||
import type { CyCookie } from './browsers/cdp_automation'
|
||||
|
||||
interface SessionData {
|
||||
cacheAcrossSpecs: boolean
|
||||
cookies: CyCookie[]
|
||||
id: string
|
||||
localStorage: object
|
||||
sessionStorage: object
|
||||
}
|
||||
const state = {
|
||||
sessions: {},
|
||||
|
||||
const state: {
|
||||
globalSessions: Record<string, SessionData>
|
||||
specSessions: Record<string, SessionData>
|
||||
} = {
|
||||
globalSessions: {},
|
||||
specSessions: {},
|
||||
}
|
||||
|
||||
export function saveSession (data: SessionData) {
|
||||
if (!data.id) throw new Error('session data had no id')
|
||||
|
||||
state.sessions[data.id] = data
|
||||
console.log('data,', data)
|
||||
if (data.cacheAcrossSpecs) {
|
||||
state.globalSessions[data.id] = data
|
||||
console.log('add as globalSessions,')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
state.specSessions[data.id] = data
|
||||
}
|
||||
|
||||
export function getGlobalSessions (): Record<string, SessionData> {
|
||||
return state.globalSessions
|
||||
}
|
||||
|
||||
export function getSession (id: string): SessionData {
|
||||
const session = state.sessions[id]
|
||||
console.log('get session....', id)
|
||||
console.log(' globalSessions....', state.globalSessions)
|
||||
console.log(' specSessions....', state.specSessions)
|
||||
const session = state.globalSessions[id] || state.specSessions[id]
|
||||
|
||||
if (!session) throw new Error(`session with id "${id}" not found`)
|
||||
|
||||
@@ -28,6 +49,11 @@ export function getState () {
|
||||
return state
|
||||
}
|
||||
|
||||
export function clearSessions () {
|
||||
state.sessions = {}
|
||||
export function clearSpecSessions () {
|
||||
state.specSessions = {}
|
||||
}
|
||||
|
||||
export function clearAllSessions () {
|
||||
state.globalSessions = {}
|
||||
state.specSessions = {}
|
||||
}
|
||||
|
||||
@@ -27,18 +27,7 @@ type StartListeningCallbacks = {
|
||||
onSocketConnection: (socket: any) => void
|
||||
}
|
||||
|
||||
type RunnerEvent =
|
||||
'reporter:restart:test:run'
|
||||
| 'runnables:ready'
|
||||
| 'run:start'
|
||||
| 'test:before:run:async'
|
||||
| 'reporter:log:add'
|
||||
| 'reporter:log:state:changed'
|
||||
| 'paused'
|
||||
| 'test:after:hooks'
|
||||
| 'run:end'
|
||||
|
||||
const runnerEvents: RunnerEvent[] = [
|
||||
const runnerEvents = [
|
||||
'reporter:restart:test:run',
|
||||
'runnables:ready',
|
||||
'run:start',
|
||||
@@ -48,18 +37,9 @@ const runnerEvents: RunnerEvent[] = [
|
||||
'paused',
|
||||
'test:after:hooks',
|
||||
'run:end',
|
||||
]
|
||||
] as const
|
||||
|
||||
type ReporterEvent =
|
||||
'runner:restart'
|
||||
| 'runner:abort'
|
||||
| 'runner:console:log'
|
||||
| 'runner:console:error'
|
||||
| 'runner:show:snapshot'
|
||||
| 'runner:hide:snapshot'
|
||||
| 'reporter:restarted'
|
||||
|
||||
const reporterEvents: ReporterEvent[] = [
|
||||
const reporterEvents = [
|
||||
// "go:to:file"
|
||||
'runner:restart',
|
||||
'runner:abort',
|
||||
@@ -68,7 +48,7 @@ const reporterEvents: ReporterEvent[] = [
|
||||
'runner:show:snapshot',
|
||||
'runner:hide:snapshot',
|
||||
'reporter:restarted',
|
||||
]
|
||||
] as const
|
||||
|
||||
const debug = Debug('cypress:server:socket-base')
|
||||
|
||||
@@ -119,7 +99,7 @@ export class SocketBase {
|
||||
// instead of throwing immediately here perhaps we need
|
||||
// to make this more resilient by automatically retrying
|
||||
// up to 1 second in the case where our automation room
|
||||
// is empty. that would give padding for reconnections
|
||||
// is empty. that would give padding for reconnection s
|
||||
// to automatically happen.
|
||||
// for instance when socket.io detects a disconnect
|
||||
// does it immediately remove the member from the room?
|
||||
@@ -336,7 +316,6 @@ export class SocketBase {
|
||||
})
|
||||
|
||||
// TODO: what to do about runner disconnections?
|
||||
|
||||
socket.on('spec:changed', (spec) => {
|
||||
return options.onSpecChanged(spec)
|
||||
})
|
||||
@@ -349,6 +328,12 @@ export class SocketBase {
|
||||
return options.onTestsReceivedAndMaybeRecord(runnables, cb)
|
||||
})
|
||||
|
||||
socket.on('get:cached:state', (cb) => {
|
||||
console.log('on get:cached:state')
|
||||
|
||||
return cb({ globalSessions: session.getGlobalSessions() })
|
||||
})
|
||||
|
||||
socket.on('mocha', (...args: unknown[]) => {
|
||||
return options.onMocha.apply(options, args)
|
||||
})
|
||||
@@ -452,17 +437,33 @@ export class SocketBase {
|
||||
case 'task':
|
||||
return task.run(cfgFile ?? null, args[0])
|
||||
case 'save:session':
|
||||
console.log('save:session', args)
|
||||
|
||||
return session.saveSession(args[0])
|
||||
case 'clear:session':
|
||||
return session.clearSessions()
|
||||
case 'clear:spec:sessions':
|
||||
console.log('clear spec sessons')
|
||||
|
||||
return session.clearSpecSessions() // pass in arg for boolean for clear all?
|
||||
// return session.clearSessions(args[0])
|
||||
case 'clear:all:sessions':
|
||||
console.log('clear all sessons')
|
||||
|
||||
return session.clearAllSessions()
|
||||
case 'get:session':
|
||||
return session.getSession(args[0])
|
||||
|
||||
case 'reset:session:state':
|
||||
cookieJar.removeAllCookies()
|
||||
session.clearSessions()
|
||||
session.clearAllSessions()
|
||||
resetRenderedHTMLOrigins()
|
||||
|
||||
return
|
||||
case 'get:cached:state':
|
||||
console.log('backend request....get:cached:state')
|
||||
|
||||
return {
|
||||
sessions: session.getGlobalSessions(),
|
||||
}
|
||||
case 'get:rendered:html:origins':
|
||||
return options.getRenderedHTMLOrigins()
|
||||
case 'reset:rendered:html:origins': {
|
||||
@@ -564,7 +565,8 @@ export class SocketBase {
|
||||
})
|
||||
|
||||
runnerEvents.forEach((event) => {
|
||||
socket.on(event, (data) => {
|
||||
socket.on(event, (data): any => {
|
||||
console.log('event', event)
|
||||
this.toReporter(event, data)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user