feat: cache sessions between specs

This commit is contained in:
Emily Rohrbough
2022-08-05 08:58:55 -05:00
parent 4d3ad9edaa
commit 6a9932315d
26 changed files with 485 additions and 170 deletions
+3 -1
View File
@@ -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()
+7 -5
View File
@@ -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 () {
+35 -24
View File
@@ -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) {
+35 -39
View File
@@ -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()
+6 -3
View File
@@ -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()
}
+1
View File
@@ -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({
+23 -5
View File
@@ -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
}
+1
View File
@@ -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,
+1
View File
@@ -13,6 +13,7 @@ declare namespace Cypress {
interface SessionData {
id: string
cacheAcrossSpecs: boolean
cookies?: Array<Cypress.Cookie> | null
localStorage?: Array<LocalStorage> | null
setup: () => void
+44
View File
@@ -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
"""
+32
View File
@@ -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"""
+2
View File
@@ -312,6 +312,8 @@ export class Http {
}
getRenderedHTMLOrigins = () => {
console.log('renderedHTMLOrigins', this.renderedHTMLOrigins)
return this.renderedHTMLOrigins
}
+2 -2
View File
@@ -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) => {
+1 -1
View File
@@ -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'
>
+1 -1
View File
@@ -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
+1 -2
View File
@@ -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,
+32 -6
View File
@@ -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 = {}
}
+32 -30
View File
@@ -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)
})
})