From 77ab6a51a0de1929171a2275e9cec9580c57241d Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Tue, 3 May 2022 17:58:56 +0200 Subject: [PATCH 01/22] fix: head content reset, fix #19721 (#21291) Co-authored-by: Zachary Williams --- npm/vue/src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/npm/vue/src/index.ts b/npm/vue/src/index.ts index 2476ba70c29e..6671a7d0d9e5 100644 --- a/npm/vue/src/index.ts +++ b/npm/vue/src/index.ts @@ -39,8 +39,6 @@ type CyMountOptions = Omit, 'atta } } & Partial -let initialInnerHtml = '' - Cypress.on('run:start', () => { // `mount` is designed to work with component testing only. // it assumes ROOT_ID exists, which is not the case in e2e. @@ -52,7 +50,6 @@ Cypress.on('run:start', () => { return } - initialInnerHtml = document.head.innerHTML Cypress.on('test:before:run', () => { Cypress.vueWrapper?.unmount() // @ts-ignore @@ -64,7 +61,6 @@ Cypress.on('run:start', () => { } el.innerHTML = '' - document.head.innerHTML = initialInnerHtml }) }) From 09bc8dcb28701a72f16aafaebc4bef52b49410c2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 3 May 2022 12:45:59 -0400 Subject: [PATCH 02/22] chore: release @cypress/vue-v3.1.2 [skip ci] --- npm/vue/CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/npm/vue/CHANGELOG.md b/npm/vue/CHANGELOG.md index 36a6bc47d375..a9e7775c1e96 100644 --- a/npm/vue/CHANGELOG.md +++ b/npm/vue/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/vue-v3.1.2](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.1.1...@cypress/vue-v3.1.2) (2022-05-03) + + +### Bug Fixes + +* head content reset, fix [#19721](https://github.com/cypress-io/cypress/issues/19721) ([#21291](https://github.com/cypress-io/cypress/issues/21291)) ([77ab6a5](https://github.com/cypress-io/cypress/commit/77ab6a51a0de1929171a2275e9cec9580c57241d)) + # [@cypress/vue-v3.1.1](https://github.com/cypress-io/cypress/compare/@cypress/vue-v3.1.0...@cypress/vue-v3.1.1) (2022-02-10) From cb14ae62e7c4362d759ceb450f4af518ee486f90 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 3 May 2022 14:01:03 -0500 Subject: [PATCH 03/22] chore(sessions): break out sessions manager code and add unit tests (#21268) Co-authored-by: Chris Breiding Co-authored-by: Bill Glesias --- .../commands/sessions/manager_spec.ts | 355 ++++++++++++++++++ .../driver/src/cy/commands/sessions/index.ts | 335 ++--------------- .../src/cy/commands/sessions/manager.ts | 295 +++++++++++++++ 3 files changed, 677 insertions(+), 308 deletions(-) create mode 100644 packages/driver/cypress/integration/commands/sessions/manager_spec.ts create mode 100644 packages/driver/src/cy/commands/sessions/manager.ts 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..be10eb7625df --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/manager_spec.ts @@ -0,0 +1,355 @@ +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 + CypressInstance = new $Cypress() + baseUrl = Cypress.config('baseUrl') + }) + + it('creates SessionsManager instance', () => { + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + + expect(sessionsManager).to.haveOwnProperty('cy') + expect(sessionsManager).to.haveOwnProperty('Cypress') + expect(sessionsManager).to.haveOwnProperty('currentTestRegisteredSessions') + expect(sessionsManager.currentTestRegisteredSessions).to.be.instanceOf(Map) + }) + + describe('.setActiveSession()', () => { + it('adds session when none 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 sessionsManager = new SessionsManager(CypressInstance, cy) + + sessionsManager.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 were 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 sessionsManager = new SessionsManager(CypressInstance, cy) + + sessionsManager.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 sessionsManager = new SessionsManager(CypressInstance, cy) + + const activeSession = sessionsManager.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 sessionsManager = new SessionsManager(CypressInstance, cy) + + let activeSession = sessionsManager.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 sessionsManager = new SessionsManager(CypressInstance, cy) + + sessionsManager.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 sessionsManager = new SessionsManager(CypressInstance, cy) + + sessionsManager.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()', () => { + it('maps when requesting all 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('*') + + 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 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 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 + }) + }) + + // TODO: + describe('._setStorageOnOrigins()', () => {}) + + it('.getAllHtmlOrigins()', async () => { + const storedOrigins = { + 'https://example.com': {}, + 'https://foobar.com': {}, + } + + storedOrigins[`${baseUrl}`] = {} + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins) + const sessionsManager = new SessionsManager(CypressInstance, cy) + + 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]) + }) + + 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 }) + }) + + it('sessions.clearAllSavedSessions()', async () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').withArgs('clear:session').resolves(null) + + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessionsManager, 'clearActiveSessions') + + await sessionsManager.sessions.clearAllSavedSessions() + + expect(sessionsSpy).to.be.calledOnce + expect(cypressSpy).to.be.calledOnceWith('clear:session', null) + }) + + it('.clearCurrentSessionData()', async () => { + // 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(window.localStorage).of.have.lengthOf(0) + expect(window.sessionStorage).of.have.lengthOf(0) + }) + + // TODO: + describe('sessions.setSessionData', () => {}) + + it('sessions.getCookies()', async () => { + const cookies = [{ id: 'cookie' }] + const cypressSpy = cy.stub(CypressInstance, 'automation').withArgs('get:cookies').resolves(cookies) + + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + + const sessionCookies = await sessionsManager.sessions.getCookies() + + 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') + + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + + await sessionsManager.sessions.setCookies({}) + + 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([]) + + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + const sessionsSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves(cookies) + + await sessionsManager.sessions.clearCookies() + + 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' }]) + + const sessData = await sessionsManager.sessions.getCurrentSessionData() + + expect(sessData).to.deep.eq({ + localStorage: [], + cookies: [{ id: 'cookie' }], + }) + + expect(getStorageSpy).to.be.calledOnce + expect(cookiesSpy).to.be.calledOnce + }) + + it('sessions.getSession()', () => { + const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:session') + + const sessionsManager = new SessionsManager(CypressInstance, () => {}) + + sessionsManager.sessions.getSession('session_1') + + expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') + }) + + // TODO: + describe('sessions.getStorage', () => {}) + + // TODO: + describe('sessions.clearStorage', () => {}) + + // 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 b7268891eecc..ad998d43dd4b 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -1,337 +1,55 @@ 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 './manager' 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: + * 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 - * - * 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 experimental session opt-in flag (removed in 9.6.0) 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 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 = 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 sessionsManager = new SessionsManager(Cypress, cy) + const sessions = sessionsManager.sessions - 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() - } + Cypress.on('run:start', () => { + Cypress.on('test:before:run:async', () => { + if (Cypress.config('experimentalSessionAndOrigin')) { + sessionsManager.currentTestRegisteredSessions.clear() - if (currentOrigin.startsWith('https:')) { - _.remove(origins, (v) => v.startsWith('http:')) + return navigateAboutBlank(false) + .then(() => sessions.clearCurrentSessionData()) + .then(() => { + return Cypress.backend('reset:rendered:html:origins') + }) } - 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 - }) - }, - } - - Cypress.on('run:start', () => { - sessions.registerSessionHooks() + return + }) }) Commands.addAll({ @@ -374,17 +92,18 @@ export default function (Commands, Cypress, cy) { }) } - let existingSession: SessionData = getActiveSession(id) + let existingSession: SessionData = sessionsManager.getActiveSession(id) + const isRegisteredSessionForTest = sessionsManager.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 } }) } @@ -394,7 +113,7 @@ export default function (Commands, Cypress, cy) { validate: options.validate, }) - currentTestRegisteredSessions.set(id, true) + sessionsManager.currentTestRegisteredSessions.set(id, true) } } @@ -447,7 +166,7 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, data) existingSession.hydrated = true - setActiveSession({ [existingSession.id]: existingSession }) + sessionsManager.setActiveSession({ [existingSession.id]: existingSession }) dataLog.set({ consoleProps: () => getConsoleProps(existingSession), diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts new file mode 100644 index 000000000000..3b63c3ad3a1c --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -0,0 +1,295 @@ +import _ from 'lodash' +import { $Location } from '../../../cypress/location' + +import { + getCurrentOriginStorage, + setPostMessageLocalStorage, + getPostMessageLocalStorage, +} from './utils' + +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 + 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') || {} + const clearedSessions: ActiveSessions = _.mapValues(curSessions, (v) => ({ ...v, hydrated: false })) + + this.cy.state('activeSessions', clearedSessions) + } + + mapOrigins = async (origins: string | Array): Promise> => { + const getOrigins = this.Cypress.Promise.map( + ([] as string[]).concat(origins), async (v) => { + if (v === '*') { + return await this.getAllHtmlOrigins() + } + + if (v === 'currentOrigin') { + return $Location.create(window.location.href).origin + } + + return $Location.create(v).origin + }, + ) + + return _.uniq(_.flatten(await getOrigins)) + } + + _setStorageOnOrigins = async (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) + } + + 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] + + return _.uniq(origins) + } + + // 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 }) + + return sess_state + }, + + clearAllSavedSessions: async () => { + this.clearActiveSessions() + + return this.Cypress.backend('clear:session', null) + }, + + clearCurrentSessionData: async () => { + window.localStorage.clear() + window.sessionStorage.clear() + + await Promise.all([ + this.sessions.clearStorage(), + this.sessions.clearCookies(), + ]) + }, + + setSessionData: async (data) => { + await this.sessions.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.sessions.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), + this.Cypress.automation('clear:cookies', null), + ]) + + await this.sessions.setCookies(data.cookies) + }, + + getCookies: async () => { + return this.Cypress.automation('get:cookies', {}) + }, + + setCookies: async (cookies) => { + return this.Cypress.automation('set:cookies', cookies) + }, + + clearCookies: async () => { + return this.Cypress.automation('clear:cookies', await this.sessions.getCookies()) + }, + + getCurrentSessionData: async () => { + const [storage, cookies] = await Promise.all([ + this.sessions.getStorage({ origin: '*' }), + this.sessions.getCookies(), + ]) + + 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') + } + + const opts = _.defaults({}, options, { + origin: 'currentOrigin', + }) + + const currentOrigin = $Location.create(window.location.href).origin + + const origins: Array = await this.mapOrigins(opts.origin) + + 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 results + } + + 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 results + }, + + clearStorage: async () => { + const origins = await this.getAllHtmlOrigins() + + const originOptions = origins.map((v) => ({ origin: v, clear: true })) + + await this.sessions.setStorage({ + localStorage: originOptions, + sessionStorage: originOptions, + }) + }, + + setStorage: async (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) + }, + } +} From 05ef83a87e8b17dee9e343aa88ae4f756979ac43 Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Thu, 5 May 2022 12:32:00 -0400 Subject: [PATCH 04/22] fix(launcher): support Firefox as a snap (#21328) --- packages/launcher/lib/linux/index.ts | 46 +++++++++++++++- packages/launcher/package.json | 4 +- packages/launcher/test/unit/linux_spec.ts | 66 ++++++++++++++++++++++- yarn.lock | 53 +++++++++++------- 4 files changed, 145 insertions(+), 24 deletions(-) diff --git a/packages/launcher/lib/linux/index.ts b/packages/launcher/lib/linux/index.ts index c09fca1dd308..d8b0410fba71 100644 --- a/packages/launcher/lib/linux/index.ts +++ b/packages/launcher/lib/linux/index.ts @@ -3,8 +3,36 @@ import type { FoundBrowser, Browser, PathData } from '../types' import { notInstalledErr } from '../errors' import { utils } from '../utils' import os from 'os' +import { promises as fs } from 'fs' import path from 'path' import Bluebird from 'bluebird' +import which from 'which' + +async function isFirefoxSnap (binary: string): Promise { + try { + return await Bluebird.resolve((async () => { + const binaryPath = await which(binary) + + // if the bin path or what it's symlinked to start with `/snap/bin`, it's a snap + if (binaryPath.startsWith('/snap/bin/') || (await fs.realpath(binaryPath)).startsWith('/snap/bin')) return true + + // read the first 16kb, don't read the entire file into memory in case it is a binary + const fd = await fs.open(binaryPath, 'r') + // @ts-ignore - needs @types/node at least 16 + // https://github.com/cypress-io/cypress/issues/21329 + const { buffer, bytesRead } = await fd.read({ length: 16384 }) + + await fd.close() + + return buffer.slice(0, bytesRead).toString('utf8').includes('exec /snap/bin/firefox') + })()) + .timeout(30000) + } catch (err) { + log('failed to check if Firefox is a snap, assuming it isn\'t %o', { err, binary }) + + return false + } +} function getLinuxBrowser ( name: string, @@ -43,11 +71,25 @@ function getLinuxBrowser ( throw notInstalledErr(binary) } - const maybeSetSnapProfilePath = (versionString: string) => { - if (os.platform() === 'linux' && name === 'chromium' && versionString.endsWith('snap')) { + const maybeSetSnapProfilePath = async (versionString: string) => { + if (os.platform() !== 'linux') return + + if (name === 'chromium' && versionString.endsWith('snap')) { // when running as a snap, chromium can only write to certain directories // @see https://github.com/cypress-io/cypress/issues/7020 + log('chromium is running as a snap, changing profile path') foundBrowser.profilePath = path.join(os.homedir(), 'snap', 'chromium', 'current') + + return + } + + if (name === 'firefox' && (await isFirefoxSnap(binary))) { + // if the binary in the path points to a script that calls the snap, set a snap-specific profile path + // @see https://github.com/cypress-io/cypress/issues/19793 + log('firefox is running as a snap, changing profile path') + foundBrowser.profilePath = path.join(os.homedir(), 'snap', 'firefox', 'current') + + return } } diff --git a/packages/launcher/package.json b/packages/launcher/package.json index 065b990a924e..6c8340e7c228 100644 --- a/packages/launcher/package.json +++ b/packages/launcher/package.json @@ -18,13 +18,15 @@ "fs-extra": "9.1.0", "lodash": "^4.17.21", "plist": "3.0.5", - "semver": "7.3.5" + "semver": "7.3.5", + "which": "2.0.2" }, "devDependencies": { "@packages/ts": "0.0.0-development", "chai": "3.5.0", "chai-as-promised": "7.1.1", "mocha": "3.5.3", + "mock-fs": "5.1.2", "shelljs": "0.8.5", "sinon": "^10.0.0", "sinon-chai": "3.4.0", diff --git a/packages/launcher/test/unit/linux_spec.ts b/packages/launcher/test/unit/linux_spec.ts index cc8db36aa36f..9170defa427f 100644 --- a/packages/launcher/test/unit/linux_spec.ts +++ b/packages/launcher/test/unit/linux_spec.ts @@ -10,12 +10,13 @@ import { expect } from 'chai' import { utils } from '../../lib/utils' import os from 'os' import sinon, { SinonStub } from 'sinon' +import mockFs from 'mock-fs' describe('linux browser detection', () => { let execa: SinonStub + let cachedEnv = { ...process.env } beforeEach(() => { - sinon.restore() execa = sinon.stub(utils, 'getOutput') sinon.stub(os, 'platform').returns('linux') @@ -34,6 +35,12 @@ describe('linux browser detection', () => { .resolves({ stdout: 'foo-browser v9001.1.2.3' }) }) + afterEach(() => { + Object.assign(process.env, cachedEnv) + mockFs.restore() + sinon.restore() + }) + it('detects browser by running --version', () => { const goal = goalBrowsers[0] const checkBrowser = (browser) => { @@ -72,6 +79,63 @@ describe('linux browser detection', () => { return detect().then(checkBrowser) }) + // https://github.com/cypress-io/cypress/issues/19793 + context('sets profilePath on snapcraft firefox', () => { + const expectedSnapFirefox = { + channel: 'stable', + name: 'firefox', + family: 'firefox', + displayName: 'Firefox', + majorVersion: 99, + minSupportedVersion: 86, + path: 'firefox', + profilePath: '/home/foo/snap/firefox/current', + version: '99.2.3', + } + + beforeEach(() => { + execa.withArgs('firefox', ['--version']) + .resolves({ stdout: 'Mozilla Firefox 99.2.3' }) + + sinon.stub(os, 'homedir').returns('/home/foo') + }) + + it('with shim script', async () => { + process.env.PATH = '/bin' + mockFs({ + '/bin/firefox': mockFs.symlink({ path: '/usr/bin/firefox' }), + '/usr/bin/firefox': mockFs.file({ mode: 0o777, content: 'foo bar foo bar foo bar\nexec /snap/bin/firefox\n' }), + }) + + const [browser] = await detect() + + expect(browser).to.deep.equal(expectedSnapFirefox) + }) + + it('with /snap/bin in path', async () => { + process.env.PATH = '/bin:/snap/bin' + mockFs({ + '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), + }) + + const [browser] = await detect() + + expect(browser).to.deep.equal(expectedSnapFirefox) + }) + + it('with symlink to /snap/bin in path', async () => { + process.env.PATH = '/bin' + mockFs({ + '/bin/firefox': mockFs.symlink({ path: '/snap/bin/firefox' }), + '/snap/bin/firefox': mockFs.file({ mode: 0o777, content: 'binary' }), + }) + + const [browser] = await detect() + + expect(browser).to.deep.equal(expectedSnapFirefox) + }) + }) + // https://github.com/cypress-io/cypress/issues/6669 it('detects browser if the --version stdout is multiline', () => { execa.withArgs('multiline-foo', ['--version']) diff --git a/yarn.lock b/yarn.lock index 2a714f8a300f..ab972e1c01ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4140,7 +4140,7 @@ "@jest/types@^26.3.0", "@jest/types@^26.6.2": version "26.6.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -8268,7 +8268,7 @@ "@types/cheerio@*", "@types/cheerio@0.22.21": version "0.22.21" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== dependencies: "@types/node" "*" @@ -8370,7 +8370,7 @@ "@types/enzyme@*", "@types/enzyme@3.10.5": version "3.10.5" - resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" + resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== dependencies: "@types/cheerio" "*" @@ -10818,9 +10818,9 @@ ansi-regex@^3.0.0: integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + version "4.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.0, ansi-regex@^5.0.1: version "5.0.1" @@ -14123,7 +14123,7 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" -color-convert@^1.9.0, color-convert@^1.9.1: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -14147,7 +14147,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@1.5.5, color-string@^1.5.4: +color-string@1.5.5: version "1.5.5" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" integrity sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg== @@ -14155,18 +14155,26 @@ color-string@1.5.5, color-string@^1.5.4: color-name "^1.0.0" simple-swizzle "^0.2.2" +color-string@^1.6.0: + version "1.9.1" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + color-support@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== color@^3.0.0, color@^3.1.1: - version "3.1.3" - resolved "https://registry.yarnpkg.com/color/-/color-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" - integrity sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== dependencies: - color-convert "^1.9.1" - color-string "^1.5.4" + color-convert "^1.9.3" + color-string "^1.6.0" colorette@^1.1.0, colorette@^1.2.1, colorette@^1.2.2: version "1.2.2" @@ -27246,6 +27254,11 @@ mock-fs@5.1.1: resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.1.tgz#d4c95e916abf400664197079d7e399d133bb6048" integrity sha512-p/8oZ3qvfKGPw+4wdVCyjDxa6wn2tP0TCf3WXC1UyUBAevezPn1TtOoxtMYVbZu/S/iExg+Ghed1busItj2CEw== +mock-fs@5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" + integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== + mock-require@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/mock-require/-/mock-require-3.0.3.tgz#ccd544d9eae81dd576b3f219f69ec867318a1946" @@ -27755,9 +27768,9 @@ node-addon-api@^1.6.3: integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== node-addon-api@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" - integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== + version "3.2.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" + integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== node-dir@^0.1.10: version "0.1.17" @@ -35316,7 +35329,7 @@ socket.io-client@4.0.1: socket.io-parser@4.0.4, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket.io-parser@~4.0.3, socket.io-parser@~4.0.4: version "4.0.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" + resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== dependencies: "@types/component-emitter" "^1.2.10" @@ -37950,7 +37963,7 @@ typescript@^4.2.3, typescript@^4.4.4: ua-parser-js@0.7.24, ua-parser-js@^0.7.18: version "0.7.24" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" + resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw== uc.micro@^1.0.1, uc.micro@^1.0.5: @@ -39398,7 +39411,7 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.2: vue-template-compiler@2.6.12, vue-template-compiler@^2.6.11: version "2.6.12" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" + resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg== dependencies: de-indent "^1.0.2" @@ -40324,7 +40337,7 @@ which@1.3.1, which@^1.1.1, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1. which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" From aa6f3b8039495ac37ced4ac581129a020ba66fa4 Mon Sep 17 00:00:00 2001 From: Sam Kvale Date: Thu, 5 May 2022 13:09:37 -0500 Subject: [PATCH 05/22] fix: Allow submit button to be outside of the form for implicit submission (#21279) Co-authored-by: Zach Bloomquist Co-authored-by: Zach Bloomquist --- packages/driver/cypress/fixtures/dom.html | 6 ++++++ .../commands/actions/type_special_chars_spec.js | 10 ++++++++++ packages/driver/src/cy/commands/actions/type.ts | 10 +++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/driver/cypress/fixtures/dom.html b/packages/driver/cypress/fixtures/dom.html index 6347924ceccf..819fc05167a9 100644 --- a/packages/driver/cypress/fixtures/dom.html +++ b/packages/driver/cypress/fixtures/dom.html @@ -323,6 +323,12 @@ +
+ + +
+ +
diff --git a/packages/driver/cypress/integration/commands/actions/type_special_chars_spec.js b/packages/driver/cypress/integration/commands/actions/type_special_chars_spec.js index 6f0b40e60b57..4296e08897ee 100644 --- a/packages/driver/cypress/integration/commands/actions/type_special_chars_spec.js +++ b/packages/driver/cypress/integration/commands/actions/type_special_chars_spec.js @@ -1486,6 +1486,16 @@ describe('src/cy/commands/actions/type - #type special chars', () => { cy.get('#multiple-inputs-and-button-submit input:first').type('foo{enter}') }) + it('triggers form submit when the submit button is outside of the form', function (done) { + this.$forms.find('[id="multiple-inputs-and-button-submit.outside-form"]').submit((e) => { + e.preventDefault() + + done() + }) + + cy.get('[id="multiple-inputs-and-button-submit.outside-form"] input:first').type('foo{enter}') + }) + it('causes click event on the button[type=submit]', function (done) { this.$forms.find('#multiple-inputs-and-button-submit button[type=submit]').click((e) => { e.preventDefault() diff --git a/packages/driver/src/cy/commands/actions/type.ts b/packages/driver/src/cy/commands/actions/type.ts index a168ebf88160..7d4753e79794 100644 --- a/packages/driver/src/cy/commands/actions/type.ts +++ b/packages/driver/src/cy/commands/actions/type.ts @@ -173,7 +173,15 @@ export default function (Commands, Cypress, cy, state, config) { const win = state('window') const getDefaultButtons = (form) => { - return form.find('input, button').filter((__, el) => { + const formId = CSS.escape(form.attr('id')) + const nestedButtons = form.find('input, button') + + const possibleDefaultButtons: JQuery = formId ? $dom.wrap(_.uniq([ + ...nestedButtons, + ...$dom.query('body', form.prop('ownerDocument')).find(`input[form="${formId}"], button[form="${formId}"]`), + ])) : nestedButtons + + return possibleDefaultButtons.filter((__, el) => { const $el = $dom.wrap(el) return ( From dbbad318aa9649d9254b831a845cbc3b1628c48b Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 5 May 2022 18:21:15 -0500 Subject: [PATCH 06/22] chore(sessions): add additional tests (#21338) --- .../commands/sessions/sessions.spec.js | 318 +++++++++++++++++ packages/driver/package.json | 2 +- .../driver/src/cy/commands/sessions/index.ts | 6 +- packages/driver/src/cypress.ts | 2 +- packages/driver/src/cypress/error_messages.ts | 7 +- .../cypress/integration/test_errors_spec.ts | 4 +- packages/reporter/src/attempts/attempts.tsx | 12 +- packages/reporter/src/test/test.tsx | 2 +- .../cypress/integration/retries.ui.spec.js | 2 +- .../cypress/integration/sessions.ui.spec.js | 322 ++++++++++++------ system-tests/__snapshots__/session_spec.ts.js | 29 +- system-tests/package.json | 3 +- .../e2e/cypress/integration/session_spec.js | 218 ------------ .../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 ++ 20 files changed, 682 insertions(+), 356 deletions(-) create mode 100644 packages/driver/cypress/integration/commands/sessions/sessions.spec.js 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/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js new file mode 100644 index 000000000000..f1b93ac82295 --- /dev/null +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -0,0 +1,318 @@ +const baseUrl = Cypress.config('baseUrl') + +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) + expect(result.cookies.map((v) => v.name)).members(obj.cookies || []) + expect(result.localStorage).deep.members(obj.localStorage || []) + expect(result.sessionStorage).deep.members(obj.sessionStorage || []) + }) +} + +describe('cy.session', { retries: 0 }, () => { + describe('args', () => { + it('accepts string as id', () => { + cy.session('session-id', () => {}) + cy.session({ name: 'session-id', zkey: 'val' }, () => {}) + }) + + it('accepts array as id', () => { + cy.session('session-id', () => {}) + }) + + it('accepts object as id', () => { + cy.session('session-id', () => {}) + }) + + // 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 + }) + }) + }) + + 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 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) + 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 multiple session calls with same sessionId but different options', function (done) { + 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') + + await expectCurrentSessionData({ + localStorage: [{ origin: baseUrl, value: { one: 'value' } }], + }) + + done() + }) + + cy.session('duplicate-session', () => { + // function content + window.localStorage.one = 'value' + }) + + cy.session('duplicate-session', () => { + // different function content + 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/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", diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index ad998d43dd4b..d57dfdb87fce 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -66,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)) { @@ -77,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 } }) 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 4c41358263e4..3c8d21ef1ae3 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1651,18 +1651,19 @@ export default { sessions: { validate_callback_false: { - message: 'Your `cy.session` **validate** callback {{reason}}', + 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', } }, 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/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/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/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 3c9d4561ace5..67c516799d61 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -1,146 +1,276 @@ const helpers = require('../support/helpers') +const path = require('path') const { runIsolatedCypress } = helpers.createCypress({ config: { experimentalSessionAndOrigin: true } }) -describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeight: 660 }, () => { - it('empty session with no data', () => { - runIsolatedCypress(() => { - it('t1', () => { - cy.session('blank_session', () => {}) - assert(true) - }) - }) +const validateSessionsInstrumentPanel = (sessionIds = []) => { + cy.get('.sessions-container') + .should('contain', `Sessions (${sessionIds.length})`) + .click() - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'blank_session') + sessionIds.forEach((id) => { + cy.contains('.sessions-container', id) + }) +} + +const validateCreateNewSessionGroup = () => { + cy.contains('Create New Session') + .closest('.command') + .should('have.class', 'command-is-open') + .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(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session.cy')) + + validateSessionsInstrumentPanel(['blank_session']) cy.get('.command-name-session') .first() - .find('i.successful') - .siblings() - .should('contain', '(new) blank_session') + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') + + cy.get('.command-name-session').contains('blank_session') + validateCreateNewSessionGroup() + }) + + cy.percySnapshot() + + cy.get('.command-name-session').first().click('top') + + // 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('creates new session with validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_with_validation.cy')) + + validateSessionsInstrumentPanel(['blank_session']) cy.get('.command-name-session') - .last() - .contains('blank_session') - .click() + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') + + cy.get('.command-name-session').contains('blank_session') + + validateCreateNewSessionGroup() + }) cy.percySnapshot() + + // 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') + + cy.get('.command-name-session').first().click('top') + + // 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('shows message for new, saved, and recreated session', () => { - runIsolatedCypress(() => { - const stub = Cypress.sinon.stub().callsFake(() => { - if (stub.callCount === 3) { - return false - } - }) + it('creates new session and fails validation', () => { + runIsolatedCypress(path.join(__dirname, '../../../../system-tests/projects/runner-e2e-specs/cypress/e2e/sessions/new_session_and_fails_validation.cy')) - beforeEach(() => { - cy.session('user1', () => { - window.localStorage.foo = 'val' - }, { - validate: stub, - }) - }) + validateSessionsInstrumentPanel(['blank_session']) - it('t1', () => { - expect(window.localStorage.foo).to.eq('val') - }) + cy.get('.command-name-session') + .first() + .within(() => { + cy.get('i.successful') + .siblings() + .should('contain', '(new) blank_session') - it('t2', () => { - expect(window.localStorage.foo).to.eq('val') - }) + cy.get('.command-name-session').contains('blank_session') - it('t3', () => { - expect(window.localStorage.foo).to.eq('val') - }) + validateCreateNewSessionGroup() }) + // 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') + + cy.contains('CypressError') + + cy.percySnapshot() + }) + + it('restores saved session', () => { + 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('validating new session was created') + cy.log('validate new session was created in first test') cy.get('.test').eq(0).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') + validateSessionsInstrumentPanel(['user1']) + validateCreateNewSessionGroup() + }) + + cy.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.successful') - .siblings() - .should('contain', '(new) user1') + .within(() => { + cy.get('i.pending').siblings().should('contain', '(saved) user1') - cy.get('.command-name-session') - .last() - .contains('user1') - .click() + 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-name-assert') - .should('have.class', 'command-state-passed') + cy.get('.command').should('have.length', 2) }) + }) + + it('recreates session', () => { + 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()) + + cy.log('validate new session was created in first test') + cy.get('.test').eq(0).within(() => { + validateSessionsInstrumentPanel(['user1']) - cy.log('validating previous session was used') + cy.contains('Create New Session') + }) + + cy.log('validate saved session was used in second test') cy.get('.test').eq(1).within(() => { - cy.get('.sessions-container') - .should('contain', 'Sessions (1)') - .click() - .should('contain', 'user1') + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.pending') - .siblings() - .should('contain', '(saved) user1') + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') - cy.get('.command-name-session') - .last() - .contains('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') + + validateCreateNewSessionGroup() + + 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('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()) + + 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('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.log('validate saved session was used in second test') + cy.get('.test').eq(1).within(() => { + validateSessionsInstrumentPanel(['user1']) cy.get('.command-name-session') .first() - .find('i.bad') - .siblings() - .should('contain', '(recreated) user1') + .within(() => { + cy.get('i.bad').siblings().should('contain', '(recreated) user1') - cy.get('.command-name-session') - .last() - .contains('user1') - }) + cy.get('.command-name-session').contains('user1') - cy.percySnapshot() - }) + cy.contains('Restore Saved Session') + .closest('.command') + .contains('Clear Page') + .should('have.length', 1) - 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' - }) + cy.contains('Restore Saved Session') + .closest('.command') + .contains('runSetup') + .should('not.exist') + + cy.contains('Validate Session: invalid') + + validateCreateNewSessionGroup() + + // 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.get('.runnable-err').should('have.length', 2) }) + }) - cy.get('.sessions-container').first().click() - .should('contain', 'Sessions (2)') - .should('contain', 'user1') - .should('contain', 'user2') + it('multiple sessions in a test', () => { + 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/__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", diff --git a/system-tests/projects/e2e/cypress/integration/session_spec.js b/system-tests/projects/e2e/cypress/integration/session_spec.js index a5de24bbeb1d..565e23912876 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'], - }) }) }) @@ -480,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', () => { @@ -760,77 +616,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' } }], - }) - }) -}) 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 d1436ee56e28fdd395bf66449f5bb11417d74b61 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Sat, 7 May 2022 00:47:19 +0900 Subject: [PATCH 07/22] chore: Distribute tests to desktop-gui containers. Make `desktop-gui` tests faster! (#21305) --- circle.yml | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/circle.yml b/circle.yml index ddcbfa48eba2..6d25a7b770bf 100644 --- a/circle.yml +++ b/circle.yml @@ -1351,13 +1351,27 @@ jobs: working_directory: packages/desktop-gui - run: command: | - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - yarn percy exec --parallel -- -- \ - yarn cypress:run --record --parallel --group 2x-desktop-gui + if [[ -v MAIN_RECORD_KEY ]]; then + # internal PR + CYPRESS_KONFIG_ENV=production \ + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy exec --parallel -- -- \ + yarn cypress:run --record --parallel --group 2x-desktop-gui + else + # external PR + TESTFILES=$(circleci tests glob "cypress/integration/**/*spec.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL) + echo "Test files for this machine are $TESTFILES" + + CYPRESS_KONFIG_ENV=production \ + PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn percy exec --parallel -- -- \ + yarn cypress:run --spec $TESTFILES + fi working_directory: packages/desktop-gui - verify-mocha-results - store_test_results: From 331e5412f566490e75d45d52d8111836745c336c Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Sat, 7 May 2022 07:22:49 +0900 Subject: [PATCH 08/22] chore: Re-organize tests in assertions_spec.js (#21283) Co-authored-by: Zach Bloomquist --- .../integration/commands/assertions_spec.js | 151 ++++++++---------- 1 file changed, 67 insertions(+), 84 deletions(-) diff --git a/packages/driver/cypress/integration/commands/assertions_spec.js b/packages/driver/cypress/integration/commands/assertions_spec.js index 6318a04a4e1b..74a48e597732 100644 --- a/packages/driver/cypress/integration/commands/assertions_spec.js +++ b/packages/driver/cypress/integration/commands/assertions_spec.js @@ -1151,7 +1151,7 @@ describe('src/cy/commands/assertions', () => { }) }) - context('format quotation marks', () => { + describe('message formatting', () => { const expectMarkdown = (test, message, done) => { cy.then(() => { test() @@ -1168,70 +1168,83 @@ describe('src/cy/commands/assertions', () => { }) } - it('preserves quotation marks in number strings', (done) => { - expectMarkdown(() => { - try { - expect(25).to.eq('25') - } catch (error) {} /* eslint-disable-line no-empty */ - }, - `expected **25** to equal **'25'**`, - done) - }) - - it('preserves quotation marks in empty string', (done) => { - expectMarkdown(() => { - try { - expect(42).to.eq('') - } catch (error) {} /* eslint-disable-line no-empty */ - }, - `expected **42** to equal **''**`, - done) - }) + // https://github.com/cypress-io/cypress/issues/19116 + it('text with backslashes', (done) => { + const text = '" { expectMarkdown( - () => expect(`\'cypress\'`).to.eq(`\'cypress\'`), - // ****'cypress'**** -> ** for emphasizing result string + ** for emphasizing the entire result. - `expected **'cypress'** to equal ****'cypress'****`, + () => expect(text).to.equal(text), + `expected **" { - expectMarkdown( - () => { - cy.get('body').then(($body) => { - expect($body).to.contain('div') - }) + describe('messages with quotation marks', () => { + it('preserves quotation marks in number strings', (done) => { + expectMarkdown(() => { + try { + expect(25).to.eq('25') + } catch (error) {} /* eslint-disable-line no-empty */ }, - `expected **** to contain **div**`, - done, - ) - }) + `expected **25** to equal **'25'**`, + done) + }) - it('removes quotation marks in strings', (done) => { - expectMarkdown(() => expect('cypress').to.eq('cypress'), `expected **cypress** to equal **cypress**`, done) - }) + it('preserves quotation marks in empty string', (done) => { + expectMarkdown(() => { + try { + expect(42).to.eq('') + } catch (error) {} /* eslint-disable-line no-empty */ + }, + `expected **42** to equal **''**`, + done) + }) - it('removes quotation marks in objects', (done) => { - expectMarkdown( - () => expect({ foo: 'bar' }).to.deep.eq({ foo: 'bar' }), - `expected **{ foo: bar }** to deeply equal **{ foo: bar }**`, - done, - ) - }) + it('preserves quotation marks if escaped', (done) => { + expectMarkdown( + () => expect(`\'cypress\'`).to.eq(`\'cypress\'`), + // ****'cypress'**** -> ** for emphasizing result string + ** for emphasizing the entire result. + `expected **'cypress'** to equal ****'cypress'****`, + done, + ) + }) - it('formats keys properly for "have.all.keys"', (done) => { - const person = { - name: 'Joe', - age: 20, - } + it('removes quotation marks in DOM elements', (done) => { + expectMarkdown( + () => { + cy.get('body').then(($body) => { + expect($body).to.contain('div') + }) + }, + `expected **** to contain **div**`, + done, + ) + }) - expectMarkdown( - () => expect(person).to.have.all.keys('name', 'age'), - `expected **{ name: Joe, age: 20 }** to have keys **name**, and **age**`, - done, - ) + it('removes quotation marks in strings', (done) => { + expectMarkdown(() => expect('cypress').to.eq('cypress'), `expected **cypress** to equal **cypress**`, done) + }) + + it('removes quotation marks in objects', (done) => { + expectMarkdown( + () => expect({ foo: 'bar' }).to.deep.eq({ foo: 'bar' }), + `expected **{ foo: bar }** to deeply equal **{ foo: bar }**`, + done, + ) + }) + + it('formats keys properly for "have.all.keys"', (done) => { + const person = { + name: 'Joe', + age: 20, + } + + expectMarkdown( + () => expect(person).to.have.all.keys('name', 'age'), + `expected **{ name: Joe, age: 20 }** to have keys **name**, and **age**`, + done, + ) + }) }) describe('formats strings with spaces', (done) => { @@ -1269,36 +1282,6 @@ describe('src/cy/commands/assertions', () => { }) }) - // TODO: this suite should be merged with the suite above - describe('message formatting', () => { - const expectMarkdown = (test, message, done) => { - cy.then(() => { - test() - }) - - cy.on('log:added', (attrs, log) => { - if (attrs.name === 'assert') { - cy.removeAllListeners('log:added') - - expect(log.get('message')).to.eq(message) - - done() - } - }) - } - - // https://github.com/cypress-io/cypress/issues/19116 - it('text with backslashes', (done) => { - const text = '" expect(text).to.equal(text), - `expected **" { beforeEach(function () { this.$body = cy.$$('body') From 0d958dc88d6be3128549631ea36abcf792725c67 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 9 May 2022 09:24:54 -0500 Subject: [PATCH 09/22] fix: ensure that proxy logs are updated after the xhr has actually completed (#21373) Co-authored-by: Emily Rohrbough --- .../integration/cypress/proxy-logging_spec.ts | 19 ++++++++++----- packages/driver/src/cypress/proxy-logging.ts | 24 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/packages/driver/cypress/integration/cypress/proxy-logging_spec.ts b/packages/driver/cypress/integration/cypress/proxy-logging_spec.ts index 64f09e986b2d..7b24f550cc79 100644 --- a/packages/driver/cypress/integration/cypress/proxy-logging_spec.ts +++ b/packages/driver/cypress/integration/cypress/proxy-logging_spec.ts @@ -181,15 +181,22 @@ describe('Proxy Logging', () => { } }) - const oldOnload = cy.state('server').options.onLoad + const xhr = new win.XMLHttpRequest() - cy.stub(cy.state('server').options, 'onLoad').log(false).callsFake(function (...args) { - setTimeout(() => { - oldOnload.call(this, ...args) - }, 500) + const logIncomingRequest = Cypress.ProxyLogging.logIncomingRequest + const updateRequestWithResponse = Cypress.ProxyLogging.updateRequestWithResponse + + // To simulate the xhr call landing second, we send updateRequestWithResponse immediately after + // the call is intercepted + cy.stub(Cypress.ProxyLogging, 'logIncomingRequest').log(false).callsFake(function (...args) { + logIncomingRequest.call(this, ...args) + updateRequestWithResponse.call(this, { + requestId: args[0].requestId, + status: 404, + }) }) - const xhr = new win.XMLHttpRequest() + cy.stub(Cypress.ProxyLogging, 'updateRequestWithResponse').log(false).callsFake(function () {}) xhr.open('GET', '/some-url') xhr.send() diff --git a/packages/driver/src/cypress/proxy-logging.ts b/packages/driver/src/cypress/proxy-logging.ts index 2c24381de418..892f280db5c1 100644 --- a/packages/driver/src/cypress/proxy-logging.ts +++ b/packages/driver/src/cypress/proxy-logging.ts @@ -324,13 +324,7 @@ export default class ProxyLogging { return proxyRequest } - private updateRequestWithResponse (responseReceived: BrowserResponseReceived): void { - const proxyRequest = _.find(this.proxyRequests, ({ preRequest }) => preRequest.requestId === responseReceived.requestId) - - if (!proxyRequest) { - return debug('unmatched responseReceived event %o', responseReceived) - } - + private updateProxyRequestWithResponse (proxyRequest, responseReceived) { proxyRequest.responseReceived = responseReceived proxyRequest.updateConsoleProps() @@ -343,6 +337,22 @@ export default class ProxyLogging { proxyRequest.log?.end() } + private updateRequestWithResponse (responseReceived: BrowserResponseReceived): void { + const proxyRequest = _.find(this.proxyRequests, ({ preRequest }) => preRequest.requestId === responseReceived.requestId) + + if (!proxyRequest) { + return debug('unmatched responseReceived event %o', responseReceived) + } + + if (proxyRequest.xhr && proxyRequest.xhr.xhr.readyState !== XMLHttpRequest.DONE) { + proxyRequest.xhr.xhr.addEventListener('load', () => { + this.updateProxyRequestWithResponse(proxyRequest, responseReceived) + }) + } else { + this.updateProxyRequestWithResponse(proxyRequest, responseReceived) + } + } + private updateRequestWithError (error: RequestError): void { const proxyRequest = _.find(this.proxyRequests, ({ preRequest }) => preRequest.requestId === error.requestId) From 5350a5353ca0568cec94aee7755367f98a104b50 Mon Sep 17 00:00:00 2001 From: Ryan Manuel Date: Mon, 9 May 2022 13:01:12 -0500 Subject: [PATCH 10/22] chore: release 9.6.1 (#21404) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6080eed75bc3..5c7ef86a5684 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "9.6.0", + "version": "9.6.1", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { From b66df646a0b403f57bf8d885fc0b66c983ca7186 Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Mon, 9 May 2022 18:13:39 -0400 Subject: [PATCH 11/22] chore: rename domain_fn to origin_fn (#21413) --- packages/driver/src/cross-origin/cypress.ts | 2 +- packages/driver/src/cross-origin/{domain_fn.ts => origin_fn.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/driver/src/cross-origin/{domain_fn.ts => origin_fn.ts} (100%) diff --git a/packages/driver/src/cross-origin/cypress.ts b/packages/driver/src/cross-origin/cypress.ts index 5a8d8cfb9425..388bce6bed1f 100644 --- a/packages/driver/src/cross-origin/cypress.ts +++ b/packages/driver/src/cross-origin/cypress.ts @@ -10,7 +10,7 @@ import { $Location } from '../cypress/location' import $Commands from '../cypress/commands' import { create as createLog } from '../cypress/log' import { bindToListeners } from '../cy/listeners' -import { handleOriginFn } from './domain_fn' +import { handleOriginFn } from './origin_fn' import { FINAL_SNAPSHOT_NAME } from '../cy/snapshots' import { handleLogs } from './events/logs' import { handleSocketEvents } from './events/socket' diff --git a/packages/driver/src/cross-origin/domain_fn.ts b/packages/driver/src/cross-origin/origin_fn.ts similarity index 100% rename from packages/driver/src/cross-origin/domain_fn.ts rename to packages/driver/src/cross-origin/origin_fn.ts From d4dfd2fe2623df9262715bbaf9f0e9b647b592a0 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 10 May 2022 17:35:14 -0500 Subject: [PATCH 12/22] chore(sessions): more driver tests (#21378) --- .../commands/sessions/sessions.spec.js | 645 ++++++++++++++++++ .../driver/src/cy/commands/sessions/index.ts | 20 +- 2 files changed, 658 insertions(+), 7 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index f1b93ac82295..dae464352fec 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -43,6 +43,651 @@ describe('cy.session', { retries: 0 }, () => { }) }) + describe('session flows', () => { + let logs = [] + let clearPageCount = 0 + let sessionGroupId + let setup + let validate + + const handleSetup = () => { + cy.then(() => { + expect(clearPageCount, 'cleared page before executing session setup').to.eq(1) + }) + + cy.contains('This is a blank page') + cy.contains('We always navigate you here after') + cy.contains('cy.session(...)') + } + + before(() => { + setup = cy.stub().callsFake(handleSetup).as('setupSession') + validate = cy.stub().as('validateSession') + }) + + const resetMocks = () => { + logs = [] + clearPageCount = 0 + sessionGroupId = undefined + setup.reset() + validate.reset() + } + + const setupTestContext = () => { + resetMocks() + + // clear all sessions only sets hydrated: false and re-using a session id + // with new setup / validation fn isn't updated/applied + Cypress.state('activeSessions', {}) + cy.log('Cypress.session.clearAllSavedSessions()') + Cypress.session.clearAllSavedSessions() + + cy.on('log:added', (attrs, log) => { + if (attrs.name === 'session' || attrs.name === 'page load' || attrs.alias?.includes('setupSession') || attrs.alias?.includes('validateSession')) { + logs.push(log) + if (!sessionGroupId) { + sessionGroupId = attrs.id + } + } + }) + + cy.on('log:changed', (attrs, log) => { + const index = logs.findIndex((l) => l.id === attrs.id) + + if (index) { + logs[index] = log + } + }) + + cy.on('internal:window:load', (args) => { + if (args.window.location.href === 'about:blank') { + clearPageCount++ + } + }) + } + + describe('create session flow', () => { + before(() => { + setupTestContext() + cy.log('create new session to test against') + cy.session('session-1', setup) + cy.url().should('eq', 'about:blank') + }) + + it('successfully creates new session', () => { + expect(setup).to.be.calledOnce + // FIXME: currently page is cleared 3 times when it should clear 2 times + expect(clearPageCount, 'total times session cleared the page').to.eq(3) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'successful', + message: '(new) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const createNewSessionGroup = logs[2].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create New Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[4].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[5].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[6].get()).to.contain({ + name: 'Clear Page', + // FIX ME... + // group: sessionGroupId, + }) + }) + + it('creates new session instrument with session details', () => { + const sessionInfo = logs[1].get('sessionInfo') + + expect(sessionInfo).to.deep.eq({ + id: 'session-1', + data: {}, + }) + }) + + it('has session details in the consoleProps', () => { + const consoleProps = logs[1].get('consoleProps')() + + expect(consoleProps).to.deep.eq({ + Command: 'session', + id: 'session-1', + table: [], + }) + }) + }) + + describe('create session with validation flow', () => { + before(() => { + setupTestContext() + cy.log('create new session with validation to test against') + + cy.session('session-1', setup, { validate }) + cy.url().should('eq', 'about:blank') + }) + + it('successfully creates new session and validates it', () => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledOnce + // FIXME: currently page is cleared 3 times when it should clear twice + expect(clearPageCount, 'total times session cleared the page').to.eq(3) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'successful', + message: '(new) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const createNewSessionGroup = logs[2].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create New Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[4].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[5].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + const validateSessionGroup = logs[6].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate Session: valid', + // FIXME.... + // group: sessionGroupId, + }) + + expect(logs[7].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + expect(logs[8].get()).to.contain({ + name: 'Clear Page', + // FIXME... + // group: sessionGroupId, + }) + }) + }) + + describe('create session with failed validation flow', () => { + it('fails validation and logs correctly', function (done) { + setupTestContext() + cy.log('create new session with validation to test against') + + cy.once('fail', (err) => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledOnce + expect(clearPageCount, 'total times session cleared the page').to.eq(2) + expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'successful', + message: '(new) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const createNewSessionGroup = logs[2].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create New Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[4].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[5].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + const validateSessionGroup = logs[6].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate Session', + // FIXME.... + // displayName: 'Validate Session: invalid', + // FIXME.... + // group: sessionGroupId, + }) + + done() + }) + + validate.callsFake(() => false) + + cy.session('session-1', setup, { validate }) + }) + }) + + describe('restores saved session flow', () => { + before(() => { + setupTestContext() + cy.log('create new session for test') + cy.session('session-1', setup) + .then(() => { + // reset and only test restored session + resetMocks() + }) + + cy.log('restore session to test against') + cy.session('session-1', setup) + cy.url().should('eq', 'about:blank') + }) + + it('successfully restores saved session', () => { + expect(setup).to.not.be.called + expect(validate).to.not.be.called + expect(clearPageCount, 'total times session cleared the page').to.eq(2) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'pending', + message: '(saved) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const restoreSavedSessionGroup = logs[2].get() + + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore Saved Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: restoreSavedSessionGroup.id, + }) + + expect(logs[4].get()).to.contain({ + name: 'Clear Page', + group: sessionGroupId, + }) + }) + }) + + describe('restores saved session with validation flow', () => { + before(() => { + setupTestContext() + cy.log('create new session for test') + cy.session('session-1', setup, { validate }) + .then(() => { + // reset and only test restored session + resetMocks() + }) + + cy.log('restore session to test against') + cy.session('session-1', setup, { validate }) + cy.url().should('eq', 'about:blank') + }) + + it('successfully restores saved session', () => { + expect(setup).to.not.be.called + expect(validate).to.be.calledOnce + expect(clearPageCount, 'total times session cleared the page').to.eq(2) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'pending', + message: '(saved) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const restoreSavedSessionGroup = logs[2].get() + + expect(restoreSavedSessionGroup).to.contain({ + displayName: 'Restore Saved Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: restoreSavedSessionGroup.id, + }) + + const validateSessionGroup = logs[4].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate Session: valid', + group: sessionGroupId, + }) + + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + expect(logs[6].get()).to.contain({ + name: 'Clear Page', + group: sessionGroupId, + }) + }) + }) + + describe('recreates existing session flow', () => { + before(() => { + setupTestContext() + cy.log('create new session for test') + cy.session('session-1', setup, { validate }) + .then(() => { + // reset and only test restored session + resetMocks() + validate.callsFake(() => { + if (validate.callCount === 1) { + return false + } + }) + }) + + cy.log('restore session to test against') + cy.session('session-1', setup, { validate }) + cy.url().should('eq', 'about:blank') + }) + + it('successfully recreates session', () => { + expect(setup).to.be.calledOnce + expect(validate).to.be.calledTwice + expect(clearPageCount, 'total times session cleared the page').to.eq(4) + }) + + it('groups session logs correctly', () => { + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'bad', + message: '(recreated) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const recreatedSavedSessionGroup = logs[2].get() + + expect(recreatedSavedSessionGroup).to.contain({ + displayName: 'Restore Saved Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: recreatedSavedSessionGroup.id, + }) + + const validateSessionGroup = logs[4].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate Session: invalid', + group: sessionGroupId, + }) + + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + expect(logs[6].get()).to.deep.contain({ + showError: true, + group: validateSessionGroup.id, + }) + + expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + + const createNewSessionGroup = logs[7].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create New Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[8].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[9].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[10].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + const secondValidateSessionGroup = logs[11].get() + + expect(secondValidateSessionGroup).to.contain({ + displayName: 'Validate Session: valid', + group: sessionGroupId, + }) + + expect(logs[12].get()).to.deep.contain({ + alias: ['validateSession'], + group: secondValidateSessionGroup.id, + }) + + expect(logs[13].get()).to.contain({ + name: 'Clear Page', + group: sessionGroupId, + }) + }) + }) + + describe('recreates existing session with failed validation flow', () => { + it('fails to recreate session and logs correctly', function (done) { + setupTestContext() + cy.log('create new session for test') + cy.session('session-1', setup, { validate }) + .then(() => { + // reset and only test restored session + resetMocks() + validate.callsFake(() => false) + }) + + cy.once('fail', (err) => { + expect(err.message).to.contain('Your `cy.session` **validate** callback returned false') + expect(setup).to.be.calledOnce + expect(validate).to.be.calledTwice + expect(clearPageCount, 'total times session cleared the page').to.eq(3) + + expect(logs[0].get()).to.contain({ + name: 'session', + id: sessionGroupId, + }) + + expect(logs[0].get('renderProps')()).to.contain({ + indicator: 'bad', + message: '(recreated) session-1', + }) + + expect(logs[1].get()).to.contain({ + name: 'session', + message: 'session-1', + group: sessionGroupId, + }) + + const recreatedSavedSessionGroup = logs[2].get() + + expect(recreatedSavedSessionGroup).to.contain({ + displayName: 'Restore Saved Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[3].get()).to.contain({ + name: 'Clear Page', + group: recreatedSavedSessionGroup.id, + }) + + const validateSessionGroup = logs[4].get() + + expect(validateSessionGroup).to.contain({ + displayName: 'Validate Session: invalid', + group: sessionGroupId, + }) + + expect(logs[5].get()).to.deep.contain({ + alias: ['validateSession'], + group: validateSessionGroup.id, + }) + + expect(logs[6].get()).to.deep.contain({ + showError: true, + group: validateSessionGroup.id, + }) + + expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.') + + const createNewSessionGroup = logs[7].get() + + expect(createNewSessionGroup).to.contain({ + displayName: 'Create New Session', + groupStart: true, + group: sessionGroupId, + }) + + expect(logs[8].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + expect(logs[9].get()).to.deep.contain({ + alias: ['setupSession'], + group: createNewSessionGroup.id, + }) + + expect(logs[10].get()).to.contain({ + name: 'Clear Page', + group: createNewSessionGroup.id, + }) + + const secondValidateSessionGroup = logs[11].get() + + expect(secondValidateSessionGroup).to.contain({ + displayName: 'Validate Session', + // displayName: 'Validate Session: invalid', + group: sessionGroupId, + }) + + expect(logs[12].get()).to.deep.contain({ + alias: ['validateSession'], + group: secondValidateSessionGroup.id, + }) + + done() + }) + + cy.log('restore session to test against') + cy.session('session-1', setup, { validate }) + }) + }) + }) + describe('errors', () => { let lastLog = null let logs = [] diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index d57dfdb87fce..b9e53a94f04a 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -132,7 +132,8 @@ export default function (Commands, Cypress, cy) { function runSetup (existingSession) { Cypress.log({ - name: 'Create New Session', + name: 'session', + displayName: 'Create New Session', state: 'passed', event: true, type: 'system', @@ -181,7 +182,8 @@ export default function (Commands, Cypress, cy) { // 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({ - name: 'Validate Session', + name: 'session', + displayName: 'Validate Session', message: '', snapshot: false, type: 'system', @@ -192,7 +194,8 @@ export default function (Commands, Cypress, cy) { const onSuccess = () => { validatingLog.set({ - name: 'Validate Session: valid', + name: 'session', + displayName: 'Validate Session: valid', message: '', type: 'system', event: true, @@ -297,7 +300,8 @@ export default function (Commands, Cypress, cy) { let hadValidationError = false let onValidationError: Function = (err, log) => { log.set({ - name: 'Validate Session: invalid', + name: 'session', + displayName: 'Validate Session: invalid', message: '', type: 'system', event: true, @@ -308,7 +312,8 @@ export default function (Commands, Cypress, cy) { showError: true, type: 'system', event: true, - name: '', + name: 'session', + displayName: '', message: '', }) @@ -339,7 +344,7 @@ export default function (Commands, Cypress, cy) { .then(() => { cy.then(async () => { await navigateAboutBlank() - Cypress.log({ groupEnd: true, name: '', message: '', emitOnly: true }) + Cypress.log({ groupEnd: true, name: 'session', message: '', emitOnly: true }) }) }) }) @@ -367,7 +372,8 @@ export default function (Commands, Cypress, cy) { } Cypress.log({ - name: 'Restore Saved Session', + name: 'session', + displayName: 'Restore Saved Session', event: true, state: 'passed', type: 'system', From 599e0c859a9eb6fcf384f3e832f78e7014c86987 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Wed, 11 May 2022 08:11:24 -0500 Subject: [PATCH 13/22] fix(sessions): refactor flows, fix grouping bugs and align validation fail text (#21379) * chore(sessions): break out sessions manager code * manager manages registered sessions * some unit tests * add more tests and some slight clean up * . * fix run mode issue. * bind correctly for spies * remove types. not sure on the return values. * fix tests * check in dump * add some command tests * more driver tests and fix session config error * Fix parsing error argument * test for failed validation error messges * wait for diff pr * update ui tests * add more ui tests * align with 10.0 test setup * clean up * will add later * fix * fix tests * baseline for session flow tests * test for logs...lots here.... * update log attrs to be able to collect session logs for tests * refactor flows, fix grouping bugs and align validation fail text * update UI test these changes fixed. * fix test failures observed in run mode * reduce flake in log updates --- .../commands/sessions/sessions.spec.js | 19 +- .../driver/src/cy/commands/sessions/index.ts | 194 +++++++++--------- .../cypress/integration/sessions.ui.spec.js | 44 ++-- 3 files changed, 120 insertions(+), 137 deletions(-) diff --git a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js index dae464352fec..fd42f3ed8ce5 100644 --- a/packages/driver/cypress/integration/commands/sessions/sessions.spec.js +++ b/packages/driver/cypress/integration/commands/sessions/sessions.spec.js @@ -162,8 +162,7 @@ describe('cy.session', { retries: 0 }, () => { expect(logs[6].get()).to.contain({ name: 'Clear Page', - // FIX ME... - // group: sessionGroupId, + group: sessionGroupId, }) }) @@ -247,8 +246,7 @@ describe('cy.session', { retries: 0 }, () => { expect(validateSessionGroup).to.contain({ displayName: 'Validate Session: valid', - // FIXME.... - // group: sessionGroupId, + group: sessionGroupId, }) expect(logs[7].get()).to.deep.contain({ @@ -258,8 +256,7 @@ describe('cy.session', { retries: 0 }, () => { expect(logs[8].get()).to.contain({ name: 'Clear Page', - // FIXME... - // group: sessionGroupId, + group: sessionGroupId, }) }) }) @@ -317,11 +314,8 @@ describe('cy.session', { retries: 0 }, () => { const validateSessionGroup = logs[6].get() expect(validateSessionGroup).to.contain({ - displayName: 'Validate Session', - // FIXME.... - // displayName: 'Validate Session: invalid', - // FIXME.... - // group: sessionGroupId, + displayName: 'Validate Session: invalid', + group: sessionGroupId, }) done() @@ -669,8 +663,7 @@ describe('cy.session', { retries: 0 }, () => { const secondValidateSessionGroup = logs[11].get() expect(secondValidateSessionGroup).to.contain({ - displayName: 'Validate Session', - // displayName: 'Validate Session: invalid', + displayName: 'Validate Session: invalid', group: sessionGroupId, }) diff --git a/packages/driver/src/cy/commands/sessions/index.ts b/packages/driver/src/cy/commands/sessions/index.ts index b9e53a94f04a..a20bd98e841f 100644 --- a/packages/driver/src/cy/commands/sessions/index.ts +++ b/packages/driver/src/cy/commands/sessions/index.ts @@ -130,7 +130,7 @@ export default function (Commands, Cypress, cy) { message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`, }) - function runSetup (existingSession) { + function createSession (existingSession, recreateSession = false) { Cypress.log({ name: 'session', displayName: 'Create New Session', @@ -141,17 +141,20 @@ export default function (Commands, Cypress, cy) { groupStart: true, }) - if (!hadValidationError) { - _log.set({ - renderProps: () => { - return { - indicator: 'successful', - message: `(new) ${_log.get().message}`, - } - }, - }) + let renderProps = { + indicator: 'successful', + message: `(new) ${_log.get().message}`, } + if (recreateSession) { + renderProps = { + indicator: 'bad', + message: `(recreated) ${_log.get().message}`, + } + } + + _log.set({ renderProps: () => renderProps }) + return cy.then(async () => { await navigateAboutBlank() await sessions.clearCurrentSessionData() @@ -179,8 +182,44 @@ export default function (Commands, Cypress, cy) { }) } + function restoreSession (existingSession) { + Cypress.log({ + name: 'session', + displayName: 'Restore Saved Session', + event: true, + state: 'passed', + type: 'system', + message: ``, + groupStart: true, + }) + + return cy.then(async () => { + await navigateAboutBlank() + + _log.set({ + renderProps: () => { + return { + indicator: 'pending', + message: `(saved) ${_log.get().message}`, + } + }, + }) + + dataLog.set({ + consoleProps: () => getConsoleProps(existingSession), + }) + + await sessions.setSessionData(existingSession) + Cypress.log({ groupEnd: true, emitOnly: true }) + }) + } + // uses Cypress hackery to resolve `false` if validate() resolves/returns false or throws/fails a cypress command. function validateSession (existingSession, _onFail) { + if (!existingSession.validate) { + return + } + const validatingLog = Cypress.log({ name: 'session', displayName: 'Validate Session', @@ -193,19 +232,14 @@ export default function (Commands, Cypress, cy) { }) const onSuccess = () => { - validatingLog.set({ - name: 'session', - displayName: 'Validate Session: valid', - message: '', - type: 'system', - event: true, - state: 'warning', - }) + validatingLog.set({ displayName: 'Validate Session: valid' }) Cypress.log({ groupEnd: true, emitOnly: true }) } const onFail = (err) => { + validatingLog.set({ displayName: 'Validate Session: invalid' }) + _onFail(err, validatingLog) } @@ -297,65 +331,66 @@ export default function (Commands, Cypress, cy) { return _catchCommand } - let hadValidationError = false - let onValidationError: Function = (err, log) => { - log.set({ - name: 'session', - displayName: 'Validate Session: invalid', - message: '', - type: 'system', - event: true, - state: 'warning', - }) - - const errorLog = Cypress.log({ + const onRestoreSessionValidationError = (err, log) => { + // create error log to show validation error to the user in the reporter + Cypress.log({ showError: true, type: 'system', event: true, name: 'session', displayName: '', message: '', - }) + }).error(err) - errorLog.error(err) - errorLog.set({ - state: 'warn', + log.endGroup() - }) + const recreateSession = true - _log.set({ - renderProps: () => { - return { - indicator: 'bad', - message: `(recreated) ${_log.get().message}`, - } - }, - }) + return createSessionWorkflow(existingSession, recreateSession) + } - Cypress.log({ groupEnd: true, emitOnly: true }) + const throwValidationError = (err, log) => { + log.endGroup() + $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) - hadValidationError = true + cy.fail(err) + } - return runSetup(existingSession) + /** + * Creates session flow: + * 1. create session + * 2. validate session + */ + const createSessionWorkflow = (existingSession, recreateSession = false) => { + return createSession(existingSession, recreateSession) .then(() => { - cy.then(() => { - return validateSession(existingSession, throwValidationError) - }) - .then(() => { - cy.then(async () => { - await navigateAboutBlank() - Cypress.log({ groupEnd: true, name: 'session', message: '', emitOnly: true }) - }) - }) + validateSession(existingSession, throwValidationError) }) } - 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) - - cy.fail(err) + /** + * Restore session flow: + * 1. restore session + * 2. validation session + * 3. if validation fails, catch error and recreate session + */ + const restoreSessionWorkflow = (existingSession) => { + return restoreSession(existingSession) + .then(() => { + validateSession(existingSession, onRestoreSessionValidationError) + }) } + /** + * Session command rules: + * If session does not exists or was no previously saved to the server, create session + * 1. run create session flow + * 2. clear page + * + * If session exists or has been saved to the server, restore session + * 1. run restore session flow + * 2. clear page + */ return cy.then(async () => { if (!existingSession.hydrated) { const serverStoredSession = await sessions.getSession(existingSession.id).catch(_.noop) @@ -365,50 +400,15 @@ export default function (Commands, Cypress, cy) { _.extend(existingSession, _.omit(serverStoredSession, 'setup')) existingSession.hydrated = true } else { - onValidationError = throwValidationError - - return runSetup(existingSession) + return createSessionWorkflow(existingSession) } } - Cypress.log({ - name: 'session', - displayName: '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) + return restoreSessionWorkflow(existingSession) }) .then(async () => { + await navigateAboutBlank() 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/packages/runner/cypress/integration/sessions.ui.spec.js b/packages/runner/cypress/integration/sessions.ui.spec.js index 67c516799d61..6ccb8ffc8d5e 100644 --- a/packages/runner/cypress/integration/sessions.ui.spec.js +++ b/packages/runner/cypress/integration/sessions.ui.spec.js @@ -19,7 +19,7 @@ const validateCreateNewSessionGroup = () => { .should('have.class', 'command-is-open') .contains('runSetup') - cy.contains('Create New Session') + return cy.contains('Create New Session') .closest('.command') .find('.command-name-Clear-Page') .should('have.length', 2) @@ -46,9 +46,7 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.get('.command-name-session').first().click('top') - // 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) + cy.get('.command').should('have.length', 2) }) it('creates new session with validation', () => { @@ -66,22 +64,18 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.get('.command-name-session').contains('blank_session') validateCreateNewSessionGroup() + + cy.contains('Validate Session: valid') + .closest('.command') + .should('have.class', 'command-is-open') + .contains('runValidation') }) cy.percySnapshot() - // 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') - cy.get('.command-name-session').first().click('top') - // 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) + cy.get('.command').should('have.length', 2) }) it('creates new session and fails validation', () => { @@ -99,14 +93,12 @@ describe('runner/cypress sessions.ui.spec', { viewportWidth: 1000, viewportHeigh cy.get('.command-name-session').contains('blank_session') validateCreateNewSessionGroup() - }) - // 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') + cy.contains('Validate Session: invalid') + .closest('.command') + .should('have.class', 'command-is-open') + .contains('runValidation') + }) cy.contains('CypressError') @@ -251,13 +243,11 @@ 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') + .parent() + .closest('.command') + .next() + .contains('Validate Session: invalid') .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') }) From 796ac005ed15f6b9a25f0f05cc1080aa942d1800 Mon Sep 17 00:00:00 2001 From: Lachlan Miller Date: Thu, 12 May 2022 11:23:52 +1000 Subject: [PATCH 14/22] chore: change to yarn registry --- yarn.lock | 168 +++++++++++++++++++++++++++--------------------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6d09175afb3d..962e8eac2e10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -142,7 +142,7 @@ "@angular-devkit/core@12.2.10": version "12.2.10" - resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.10.tgz#3da62eceef3904f92cd3f860618b4ae513029ce2" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-12.2.10.tgz#3da62eceef3904f92cd3f860618b4ae513029ce2" integrity sha512-0qhmS7Qvl0hiRVTHxEC/ipFAfzYofPstw0ZITDpEMw+pgHlOZolOlnFrv8LyOXWNqlSIH5fS9D3WF7Hpm7ApYA== dependencies: ajv "8.6.2" @@ -177,7 +177,7 @@ "@angular-devkit/schematics-cli@^12.2.10": version "12.2.10" - resolved "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-12.2.10.tgz#91d27181176b924fa8872506d3552c183e6f2f1f" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics-cli/-/schematics-cli-12.2.10.tgz#91d27181176b924fa8872506d3552c183e6f2f1f" integrity sha512-eXzsQ4nM+g4EB0Ixh8rdN1hhm1KEnjychhITvzHgIFSLu0n6o8g36PrZgvshzvHlJViViNztm9uKjX54eaJ/Dw== dependencies: "@angular-devkit/core" "12.2.10" @@ -189,7 +189,7 @@ "@angular-devkit/schematics@11.2.12": version "11.2.12" - resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-11.2.12.tgz#a2b7b02f6677a2fa9f4f6716f0aaa87347d41509" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-11.2.12.tgz#a2b7b02f6677a2fa9f4f6716f0aaa87347d41509" integrity sha512-d1fxxkLPtP87iwAFZ0iUNWMlqULm05bbnRc5g5vrFxdtpwwybC15+NS64d3gzX0vAvnCa+cDQkjeD92bJJNjLw== dependencies: "@angular-devkit/core" "11.2.12" @@ -198,7 +198,7 @@ "@angular-devkit/schematics@12.2.10": version "12.2.10" - resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.10.tgz#b8d4031053fd76d93caa7f33aeeb67383e37f0ab" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-12.2.10.tgz#b8d4031053fd76d93caa7f33aeeb67383e37f0ab" integrity sha512-oQ2EWdkEDE+eAttHeviXsvBi85PsntQT+IffjKUZdbQU+Leuk/pKUpTeea1YosU1p4Cz3PKYF+P/Nl5Jy3B7IQ== dependencies: "@angular-devkit/core" "12.2.10" @@ -4521,7 +4521,7 @@ "@jest/types@^26.3.0": version "26.6.2" - resolved "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" @@ -6042,7 +6042,7 @@ "@octokit/core@^3.5.1": version "3.5.1" - resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b" integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== dependencies: "@octokit/auth-token" "^2.4.4" @@ -6100,14 +6100,14 @@ "@octokit/plugin-paginate-rest@^2.16.0": version "2.16.1" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.1.tgz#699ea5c4d0274626bd7c4b896b4789c21ea3540e" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.16.1.tgz#699ea5c4d0274626bd7c4b896b4789c21ea3540e" integrity sha512-53RGhlRNhQVepZq063YCoIssZkAYjIU1kWQi9m7Qjdq/1IiuZOB9iSHdjDQmKtHWDRSqNwbtsX+tlJNhP1DHEQ== dependencies: "@octokit/types" "^6.27.1" "@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.4": version "1.0.4" - resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== "@octokit/plugin-rest-endpoint-methods@2.4.0": @@ -6120,7 +6120,7 @@ "@octokit/plugin-rest-endpoint-methods@^5.9.0": version "5.10.2" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.10.2.tgz#84ae65ae3f40b2d8a25bf7db7c1054a15d9383b6" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.10.2.tgz#84ae65ae3f40b2d8a25bf7db7c1054a15d9383b6" integrity sha512-Q1QdPqA1HuKbXBuUnyNEImp948htcxgOVwUFTbUbRUsWSJPhabDe3Imd+C8vZg2czpBkl9uR8zx71WE1CP9TxA== dependencies: "@octokit/types" "^6.27.1" @@ -6137,7 +6137,7 @@ "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": version "2.1.0" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677" integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== dependencies: "@octokit/types" "^6.0.3" @@ -6158,7 +6158,7 @@ "@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.6.0": version "5.6.1" - resolved "https://registry.npmjs.org/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.1.tgz#f97aff075c37ab1d427c49082fefeef0dba2d8ce" integrity sha512-Ls2cfs1OfXaOKzkcxnqw5MR6drMA/zWX/LIS/p8Yjdz7QKTPQLMsB3R+OvoxE6XnXeXEE2X7xe4G4l4X0gRiKQ== dependencies: "@octokit/endpoint" "^6.0.1" @@ -6210,7 +6210,7 @@ "@octokit/rest@^18.0.0": version "18.10.0" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.10.0.tgz#8a0add9611253e0e31d3ed5b4bc941a3795a7648" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.10.0.tgz#8a0add9611253e0e31d3ed5b4bc941a3795a7648" integrity sha512-esHR5OKy38bccL/sajHqZudZCvmv4yjovMJzyXlphaUo7xykmtOdILGJ3aAm0mFHmMLmPFmDMJXf39cAjNJsrw== dependencies: "@octokit/core" "^3.5.1" @@ -6669,7 +6669,7 @@ "@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2": version "9.1.2" - resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c" integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== dependencies: "@sinonjs/commons" "^1.7.0" @@ -7010,7 +7010,7 @@ "@types/braces@*": version "3.0.1" - resolved "https://registry.npmjs.org/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b" + resolved "https://registry.yarnpkg.com/@types/braces/-/braces-3.0.1.tgz#5a284d193cfc61abb2e5a50d36ebbc50d942a32b" integrity sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ== "@types/cacheable-request@^6.0.1": @@ -7067,7 +7067,7 @@ "@types/cheerio@*", "@types/cheerio@0.22.21": version "0.22.21" - resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" integrity sha512-aGI3DfswwqgKPiEOTaiHV2ZPC9KEhprpgEbJnv0fZl3SGX0cGgEva1126dGrMC6AJM6v/aihlUgJn9M5DbDZ/Q== dependencies: "@types/node" "*" @@ -7179,7 +7179,7 @@ "@types/enzyme@*", "@types/enzyme@3.10.5": version "3.10.5" - resolved "https://registry.npmjs.org/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.10.5.tgz#fe7eeba3550369eed20e7fb565bfb74eec44f1f0" integrity sha512-R+phe509UuUYy9Tk0YlSbipRpfVtIzb/9BHn5pTEtjJTF5LXvUjrIQcZvNyANNEyFrd2YGs196PniNT1fgvOQA== dependencies: "@types/cheerio" "*" @@ -7391,7 +7391,7 @@ "@types/istanbul-reports@^3.0.0": version "3.0.1" - resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== dependencies: "@types/istanbul-lib-report" "*" @@ -7477,7 +7477,7 @@ "@types/micromatch@4.0.2": version "4.0.2" - resolved "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.2.tgz#ce29c8b166a73bf980a5727b1e4a4d099965151d" + resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-4.0.2.tgz#ce29c8b166a73bf980a5727b1e4a4d099965151d" integrity sha512-oqXqVb0ci19GtH0vOA/U2TmHTcRY9kuZl4mqUxe0QmJAlIW13kzhuK5pi1i9+ngav8FjpSb9FVS/GE00GLX1VA== dependencies: "@types/braces" "*" @@ -7531,7 +7531,7 @@ "@types/mocha@9.1.0": version "9.1.0" - resolved "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== "@types/mock-fs@4.10.0": @@ -7555,7 +7555,7 @@ "@types/node@*", "@types/node@>= 8", "@types/node@>=10.0.0": version "15.0.1" - resolved "https://registry.npmjs.org/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" + resolved "https://registry.yarnpkg.com/@types/node/-/node-15.0.1.tgz#ef34dea0881028d11398be5bf4e856743e3dc35a" integrity sha512-TMkXt0Ck1y0KKsGr9gJtWGjttxlZnnvDtphxUOSd0bfaR6Q1jle+sPvrzNR1urqYTWMinoKvjKfXUGsumaO1PA== "@types/node@14.14.31", "@types/node@^14.14.31", "@types/node@^14.6.2": @@ -7742,7 +7742,7 @@ "@types/semver@7.3.9": version "7.3.9" - resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.9.tgz#152c6c20a7688c30b967ec1841d31ace569863fc" integrity sha512-L/TMpyURfBkf+o/526Zb6kd/tchUP3iBDEPjqjb+U2MAJhVRxxrmr2fwpe08E7QsV7YLcpq0tUaQ9O9x97ZIxQ== "@types/send@^0.17.1": @@ -7793,7 +7793,7 @@ "@types/sinon-chai@3.2.8": version "3.2.8" - resolved "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" + resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc" integrity sha512-d4ImIQbT/rKMG8+AXpmcan5T2/PNeSjrYhvkwet6z0p8kzYtfgA32xzOBlbU0yqJfq+/0Ml805iFoODO0LP5/g== dependencies: "@types/chai" "*" @@ -7801,7 +7801,7 @@ "@types/sinon@*", "@types/sinon@10.0.11": version "10.0.11" - resolved "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.11.tgz#8245827b05d3fc57a6601bd35aee1f7ad330fc42" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.11.tgz#8245827b05d3fc57a6601bd35aee1f7ad330fc42" integrity sha512-dmZsHlBsKUtBpHriNjlK0ndlvEh8dcb9uV9Afsbt89QIyydpC7NcR+nWlAhASfy3GHnxTl4FX/aKE7XZUt/B4g== dependencies: "@types/sinonjs__fake-timers" "*" @@ -8104,7 +8104,7 @@ "@types/yargs@^15.0.0": version "15.0.14" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-15.0.14.tgz#26d821ddb89e70492160b66d10a0eb6df8f6fb06" integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== dependencies: "@types/yargs-parser" "*" @@ -9253,7 +9253,7 @@ ajv-errors@^1.0.0: ajv-formats@2.1.0: version "2.1.0" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz#96eaf83e38d32108b66d82a9cb0cfa24886cdfeb" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.0.tgz#96eaf83e38d32108b66d82a9cb0cfa24886cdfeb" integrity sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q== dependencies: ajv "^8.0.0" @@ -9309,7 +9309,7 @@ ajv@6.5.3: ajv@8.6.2: version "8.6.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.2.tgz#2fb45e0e5fcbc0813326c1c3da535d1881bb0571" integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== dependencies: fast-deep-equal "^3.1.1" @@ -9500,7 +9500,7 @@ ansi-regex@^3.0.0: ansi-regex@^4.1.0: version "4.1.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.1.tgz#164daac87ab2d6f6db3a29875e2d1766582dabed" integrity sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g== ansi-regex@^5.0.0, ansi-regex@^5.0.1: @@ -9656,7 +9656,7 @@ append-buffer@^1.0.2: append-field@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= append-transform@^2.0.0: @@ -10024,7 +10024,7 @@ asn1.js@^5.2.0: asn1@^0.2.4, asn1@~0.2.3: version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== dependencies: safer-buffer "~2.1.0" @@ -11501,7 +11501,7 @@ before-after-hook@^1.4.0: before-after-hook@^2.0.0, before-after-hook@^2.2.0: version "2.2.2" - resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e" integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== big.js@^3.1.3: @@ -12177,7 +12177,7 @@ bundle-require@3.0.4: busboy@^0.2.11: version "0.2.14" - resolved "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= dependencies: dicer "0.2.5" @@ -13472,7 +13472,7 @@ color-string@1.5.5: color-string@^1.6.0, color-string@^1.9.0: version "1.9.1" - resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== dependencies: color-name "^1.0.0" @@ -13493,7 +13493,7 @@ color@^3.0.0: color@^4.0.1: version "4.2.3" - resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== dependencies: color-convert "^2.0.1" @@ -14301,7 +14301,7 @@ cp-file@^6.1.0: cpu-features@0.0.2: version "0.0.2" - resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz#9f636156f1155fd04bdbaa028bb3c2fbef3cea7a" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.2.tgz#9f636156f1155fd04bdbaa028bb3c2fbef3cea7a" integrity sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA== dependencies: nan "^2.14.1" @@ -15075,7 +15075,7 @@ dayjs@1.10.4, dayjs@^1.10.3, dayjs@^1.10.4, dayjs@^1.9.3: de-indent@^1.0.2: version "1.0.2" - resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0= debounce@^1.2.0: @@ -15595,7 +15595,7 @@ detect-libc@^1.0.3: detect-libc@^2.0.0: version "2.0.1" - resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== detect-newline@3.1.0: @@ -15671,7 +15671,7 @@ dezalgo@^1.0.0, dezalgo@~1.0.3: dicer@0.2.5: version "0.2.5" - resolved "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= dependencies: readable-stream "1.1.x" @@ -15847,7 +15847,7 @@ dns-txt@^2.0.2: docker-modem@^3.0.0: version "3.0.3" - resolved "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz#ac4bb1f32f81ac2e7120c5e99a068fab2458a32f" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-3.0.3.tgz#ac4bb1f32f81ac2e7120c5e99a068fab2458a32f" integrity sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw== dependencies: debug "^4.1.1" @@ -15857,7 +15857,7 @@ docker-modem@^3.0.0: dockerode@3.3.1: version "3.3.1" - resolved "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz#74f66e239e092e7910e2beae6322d35c44b08cdc" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-3.3.1.tgz#74f66e239e092e7910e2beae6322d35c44b08cdc" integrity sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ== dependencies: docker-modem "^3.0.0" @@ -16186,7 +16186,7 @@ electron-builder@^22.13.1: electron-context-menu@3.1.1: version "3.1.1" - resolved "https://registry.npmjs.org/electron-context-menu/-/electron-context-menu-3.1.1.tgz#109884e79df293f7e85effcbdbbe45d362987d94" + resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-3.1.1.tgz#109884e79df293f7e85effcbdbbe45d362987d94" integrity sha512-LJhwaKf6XHwk2LQ5SdwoGNODoA8lRwks9bbEeAqqMf4e3hsrT7pZtX6MaHKYNFZKxF14JjI/VR+VRjGvxmaQoA== dependencies: cli-truncate "^2.1.0" @@ -16195,7 +16195,7 @@ electron-context-menu@3.1.1: electron-dl@^3.2.1: version "3.2.1" - resolved "https://registry.npmjs.org/electron-dl/-/electron-dl-3.2.1.tgz#244a1316c663691f70e8aa4515cd1a401b4b5690" + resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-3.2.1.tgz#244a1316c663691f70e8aa4515cd1a401b4b5690" integrity sha512-k5DFjocJlXbrjshO1zeWe/Gz7HkGwCgnehHPemiyzN2B/LfLlnbIX7sCj5F+huTwZ2l+nQehTI4IR37xvCn6FQ== dependencies: ext-name "^5.0.0" @@ -16204,7 +16204,7 @@ electron-dl@^3.2.1: electron-is-dev@^2.0.0: version "2.0.0" - resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-2.0.0.tgz#833487a069b8dad21425c67a19847d9064ab19bd" + resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-2.0.0.tgz#833487a069b8dad21425c67a19847d9064ab19bd" integrity sha512-3X99K852Yoqu9AcW50qz3ibYBWY79/pBhlMCab8ToEWS48R0T9tyxRiQhwylE7zQdXrMnx2JKqUJyMPmt5FBqA== electron-notarize@^1.1.1: @@ -16376,7 +16376,7 @@ engine.io-client@~3.5.0: engine.io-client@~5.0.0: version "5.0.1" - resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-5.0.1.tgz#9470fc6655c9789c5c0aa1a0e7e7d9ae9753a798" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-5.0.1.tgz#9470fc6655c9789c5c0aa1a0e7e7d9ae9753a798" integrity sha512-CQtGN3YwfvbxVwpPugcsHe5rHT4KgT49CEcQppNtu9N7WxbPN0MAG27lGaem7bvtCFtGNLSL+GEqXsFSz36jTg== dependencies: base64-arraybuffer "0.1.4" @@ -16434,7 +16434,7 @@ engine.io@~3.5.0: engine.io@~5.0.0: version "5.0.0" - resolved "https://registry.npmjs.org/engine.io/-/engine.io-5.0.0.tgz#470dc94a8a4907fa4d2cd1fa6611426afcee61bf" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-5.0.0.tgz#470dc94a8a4907fa4d2cd1fa6611426afcee61bf" integrity sha512-BATIdDV3H1SrE9/u2BAotvsmjJg0t1P4+vGedImSs1lkFAtQdvk4Ev1y4LDiPF7BPWgXWEG+NDY+nLvW3UrMWw== dependencies: accepts "~1.3.4" @@ -18493,7 +18493,7 @@ find-versions@^3.0.0: find-versions@^4.0.0: version "4.0.0" - resolved "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" + resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== dependencies: semver-regex "^3.1.2" @@ -19549,7 +19549,7 @@ glob@7.1.6: glob@7.2.0, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== dependencies: fs.realpath "^1.0.0" @@ -20390,7 +20390,7 @@ he@1.1.1: he@1.2.0, he@1.2.x, he@^1.1.0, he@^1.1.1, he@^1.2.0: version "1.2.0" - resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== header-case@^1.0.0: @@ -21308,7 +21308,7 @@ inquirer@7.3.3, inquirer@^7.0.0, inquirer@^7.3.3: inquirer@8.1.2: version "8.1.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.1.2.tgz#65b204d2cd7fb63400edd925dfe428bafd422e3d" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.1.2.tgz#65b204d2cd7fb63400edd925dfe428bafd422e3d" integrity sha512-DHLKJwLPNgkfwNmsuEUKSejJFbkv0FMO9SMiQbjI3n5NQuCrSIBqP66ggqyz2a6t2qEolKrMjhQ3+W/xXgUQ+Q== dependencies: ansi-escapes "^4.2.1" @@ -21594,7 +21594,7 @@ is-color-stop@^1.0.0: is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.8.0, is-core-module@^2.8.1: version "2.8.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== dependencies: has "^1.0.3" @@ -25247,7 +25247,7 @@ marked@^1.0.0: marked@^2.0.0: version "2.0.5" - resolved "https://registry.npmjs.org/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.5.tgz#2d15c759b9497b0e7b5b57f4c2edabe1002ef9e7" integrity sha512-yfCEUXmKhBPLOzEC7c+tc4XZdIeTdGoRCZakFMkCxodr7wDXqoapIME4wjcpBPJLNyUnKJ3e8rb8wlAgnLnaDw== matchdep@^2.0.0: @@ -26300,7 +26300,7 @@ mock-fs@5.1.1: mock-fs@5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== mock-require@3.0.3: @@ -26465,7 +26465,7 @@ ms@2.1.3, ms@^2.0.0, ms@^2.1.1: multer@1.4.2: version "1.4.2" - resolved "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== dependencies: append-field "^1.0.0" @@ -26534,7 +26534,7 @@ mz@^2.4.0, mz@^2.5.0: nan@^2.10.0, nan@^2.12.1, nan@^2.14.1, nan@^2.15.0: version "2.15.0" - resolved "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== nanoid@3.1.20: @@ -26771,7 +26771,7 @@ node-abi@^2.7.0: node-abi@^3.3.0: version "3.15.0" - resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz#cd9ac8c58328129b49998cc6fa16aa5506152716" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.15.0.tgz#cd9ac8c58328129b49998cc6fa16aa5506152716" integrity sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA== dependencies: semver "^7.3.5" @@ -26788,7 +26788,7 @@ node-addon-api@^3.1.0: node-addon-api@^4.2.0: version "4.3.0" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f" integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ== node-dir@0.1.8: @@ -27929,7 +27929,7 @@ optionator@^0.9.1: ora@*, ora@5.4.1, ora@^5.1.0, ora@^5.3.0: version "5.4.1" - resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== dependencies: bl "^4.1.0" @@ -27944,7 +27944,7 @@ ora@*, ora@5.4.1, ora@^5.1.0, ora@^5.3.0: ora@5.3.0: version "5.3.0" - resolved "https://registry.npmjs.org/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== dependencies: bl "^4.0.3" @@ -28804,7 +28804,7 @@ path-key@^3.0.0, path-key@^3.1.0: path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== path-platform@~0.11.15: @@ -30159,7 +30159,7 @@ prebuild-install@^5.2.4, prebuild-install@^5.3.5: prebuild-install@^7.0.0: version "7.1.0" - resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.0.tgz#991b6ac16c81591ba40a6d5de93fb33673ac1370" integrity sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA== dependencies: detect-libc "^2.0.0" @@ -30638,7 +30638,7 @@ punycode@1.3.2: punycode@^1.2.4, punycode@^1.3.2, punycode@^1.4.1: version "1.4.1" - resolved "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= punycode@^2.1.0, punycode@^2.1.1: @@ -32283,7 +32283,7 @@ resolve@^0.6.3: resolve@^1.1.4, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.10.1, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.15.1, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.22.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1: version "1.22.0" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== dependencies: is-core-module "^2.8.1" @@ -32583,7 +32583,7 @@ rxjs@6.6.7, rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.5.3, rxjs@^6.5.4, rxjs@^6.6.0, rxj rxjs@^7.1.0, rxjs@^7.2.0: version "7.4.0" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.4.0.tgz#a12a44d7eebf016f5ff2441b87f28c9a51cebc68" integrity sha512-7SQDi7xeTMCJpqViXh8gL/lebcwlp3d831F05+9B44A4B0WfsEwUQHR64gsH1kvJ+Ep/J9K2+n1hVl1CsGN23w== dependencies: tslib "~2.1.0" @@ -33008,7 +33008,7 @@ semver@7.3.5: semver@7.3.7, semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5: version "7.3.7" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: lru-cache "^6.0.0" @@ -33249,7 +33249,7 @@ shallow-clone@^3.0.0: sharp@0.29.3: version "0.29.3" - resolved "https://registry.npmjs.org/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.29.3.tgz#0da183d626094c974516a48fab9b3e4ba92eb5c2" integrity sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA== dependencies: color "^4.0.1" @@ -33360,7 +33360,7 @@ shiki@^0.9.12: shx@0.3.3: version "0.3.3" - resolved "https://registry.npmjs.org/shx/-/shx-0.3.3.tgz#681a88c7c10db15abe18525349ed474f0f1e7b9f" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.3.tgz#681a88c7c10db15abe18525349ed474f0f1e7b9f" integrity sha512-nZJ3HFWVoTSyyB+evEKjJ1STiixGztlqwKLTUNV5KqMWtGey9fTd4KU1gdZ1X9BV6215pswQ/Jew9NsuS/fNDA== dependencies: minimist "^1.2.3" @@ -33420,7 +33420,7 @@ simple-get@^3.0.3: simple-get@^4.0.0: version "4.0.1" - resolved "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543" integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA== dependencies: decompress-response "^6.0.0" @@ -33473,7 +33473,7 @@ sinon-chai@3.5.0: sinon-chai@3.7.0, sinon-chai@^3.3.0, sinon-chai@^3.5.0: version "3.7.0" - resolved "https://registry.npmjs.org/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-3.7.0.tgz#cfb7dec1c50990ed18c153f1840721cf13139783" integrity sha512-mf5NURdUaSdnatJx3uhoBOrY9dtL19fiOtAdT1Azxg3+lNJFiuN0uzaU3xX1LeAfL17kHQhTAJgpsfhbMJMY2g== sinon@1.17.7: @@ -33488,7 +33488,7 @@ sinon@1.17.7: sinon@13.0.2, sinon@^13.0.1: version "13.0.2" - resolved "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz#c6a8ddd655dc1415bbdc5ebf0e5b287806850c3a" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-13.0.2.tgz#c6a8ddd655dc1415bbdc5ebf0e5b287806850c3a" integrity sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA== dependencies: "@sinonjs/commons" "^1.8.3" @@ -33868,7 +33868,7 @@ socket.io-adapter@~1.1.0: socket.io-adapter@~2.2.0: version "2.2.0" - resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz#43af9157c4609e74b8addc6867873ac7eb48fda2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.2.0.tgz#43af9157c4609e74b8addc6867873ac7eb48fda2" integrity sha512-rG49L+FwaVEwuAdeBRq49M97YI3ElVabJPzvHT9S6a2CWhDKnjSFasvwAwSYPRhQzfn4NtDIbCaGYgOCOU/rlg== socket.io-client@2.4.0, socket.io-client@^2.2.0: @@ -33890,7 +33890,7 @@ socket.io-client@2.4.0, socket.io-client@^2.2.0: socket.io-client@4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.0.1.tgz#8f3bf4ce9282dda1741a4ed0f726b412944e012b" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.0.1.tgz#8f3bf4ce9282dda1741a4ed0f726b412944e012b" integrity sha512-6AkaEG5zrVuSVW294cH1chioag9i1OqnCYjKwTc3EBGXbnyb98Lw7yMa40ifLjFj3y6fsFKsd0llbUZUCRf3Qw== dependencies: "@types/component-emitter" "^1.2.10" @@ -33903,7 +33903,7 @@ socket.io-client@4.0.1: socket.io-parser@4.0.4, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket.io-parser@~4.0.3, socket.io-parser@~4.0.4: version "4.0.4" - resolved "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== dependencies: "@types/component-emitter" "^1.2.10" @@ -33912,7 +33912,7 @@ socket.io-parser@4.0.4, socket.io-parser@~3.3.0, socket.io-parser@~3.4.0, socket socket.io@4.0.1: version "4.0.1" - resolved "https://registry.npmjs.org/socket.io/-/socket.io-4.0.1.tgz#d2e01cf3780d810b66182b3fbef08a04a4ccf924" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.0.1.tgz#d2e01cf3780d810b66182b3fbef08a04a4ccf924" integrity sha512-g8eZB9lV0f4X4gndG0k7YZAywOg1VxYgCUspS4V+sDqsgI/duqd0AW84pKkbGj/wQwxrqrEq+VZrspRfTbHTAQ== dependencies: "@types/cookie" "^0.4.0" @@ -34293,7 +34293,7 @@ speed-measure-webpack-plugin@1.4.2: split-ca@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" integrity sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY= split-on-first@^1.0.0: @@ -34372,7 +34372,7 @@ ssestream@1.0.1: ssh2@^1.4.0: version "1.6.0" - resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.6.0.tgz#61aebc3a6910fe488f9c85cd8355bdf8d4724e05" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.6.0.tgz#61aebc3a6910fe488f9c85cd8355bdf8d4724e05" integrity sha512-lxc+uvXqOxyQ99N2M7k5o4pkYDO5GptOTYduWw7hIM41icxvoBcCNHcj+LTKrjkL0vFcAl+qfZekthoSFRJn2Q== dependencies: asn1 "^0.2.4" @@ -34590,7 +34590,7 @@ stream-to@~0.2.0: streamsearch@0.1.2: version "0.1.2" - resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= strict-uri-encode@^1.0.0: @@ -35135,7 +35135,7 @@ supports-hyperlinks@^2.1.0: supports-preserve-symlinks-flag@^1.0.0: version "1.0.0" - resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== sver-compat@^1.5.0: @@ -35205,7 +35205,7 @@ swap-case@^2.0.2: symbol-observable@3.0.0: version "3.0.0" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-3.0.0.tgz#eea8f6478c651018e059044268375c408c15c533" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-3.0.0.tgz#eea8f6478c651018e059044268375c408c15c533" integrity sha512-6tDOXSHiVjuCaasQSWTmHUWn4PuG7qa3+1WT031yTc/swT7+rLiw3GOrFxaH1E3lLP09dH3bVuVDf2gK5rxG3Q== symbol-observable@4.0.0: @@ -35295,7 +35295,7 @@ tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: tar-fs@^2.0.0, tar-fs@^2.1.1: version "2.1.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== dependencies: chownr "^1.1.1" @@ -35305,7 +35305,7 @@ tar-fs@^2.0.0, tar-fs@^2.1.1: tar-fs@~2.0.1: version "2.0.1" - resolved "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== dependencies: chownr "^1.1.1" @@ -36229,7 +36229,7 @@ tsutils@^2.29.0: tsutils@^3.17.1: version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -36401,7 +36401,7 @@ typescript@^4.2.3, typescript@^4.4.4: ua-parser-js@0.7.24, ua-parser-js@^0.7.18: version "0.7.24" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.24.tgz#8d3ecea46ed4f1f1d63ec25f17d8568105dc027c" integrity sha512-yo+miGzQx5gakzVK3QFfN0/L9uVhosXBBO7qmnk7c2iw1IhL212wfA3zbnI54B0obGwC/5NWub/iT9sReMx+Fw== uc.micro@^1.0.1, uc.micro@^1.0.5: @@ -37555,7 +37555,7 @@ vue-router@4, vue-router@^4.0.0: vue-template-compiler@2.6.12: version "2.6.12" - resolved "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.12.tgz#947ed7196744c8a5285ebe1233fe960437fcc57e" integrity sha512-OzzZ52zS41YUbkCBfdXShQTe69j1gQDZ9HIX8miuC9C3rBCk9wIRjLiZZLrmX9V+Ftq/YEyv1JaVr5Y/hNtByg== dependencies: de-indent "^1.0.2" @@ -38314,7 +38314,7 @@ which@1.3.1, which@^1.1.1, which@^1.2.12, which@^1.2.14, which@^1.2.4, which@^1. which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" - resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" @@ -39322,7 +39322,7 @@ yargs@~3.27.0: yarn-deduplicate@3.1.0: version "3.1.0" - resolved "https://registry.npmjs.org/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e" + resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-3.1.0.tgz#3018d93e95f855f236a215b591fe8bc4bcabba3e" integrity sha512-q2VZ6ThNzQpGfNpkPrkmV7x5HT9MOhCUsTxVTzyyZB0eSXz1NTodHn+r29DlLb+peKk8iXxzdUVhQG9pI7moFw== dependencies: "@yarnpkg/lockfile" "^1.1.0" From 841d28802c32d4a779bff77e58915c02c6c83850 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Thu, 12 May 2022 11:49:24 -0500 Subject: [PATCH 15/22] chore: update stubbed cloud types (#21451) --- packages/graphql/schemas/cloud.graphql | 14 ++++++++++++++ packages/graphql/schemas/schema.graphql | 10 ++++++++++ packages/graphql/test/stubCloudTypes.ts | 2 ++ 3 files changed, 26 insertions(+) diff --git a/packages/graphql/schemas/cloud.graphql b/packages/graphql/schemas/cloud.graphql index 8e484a190684..5f6b1b0ed882 100644 --- a/packages/graphql/schemas/cloud.graphql +++ b/packages/graphql/schemas/cloud.graphql @@ -228,6 +228,7 @@ type CloudRun implements Node { """ id: ID! status: CloudRunStatus + tags: [CloudRunTag] """ Total duration of the run in milliseconds, accounting for any parallelization @@ -239,6 +240,11 @@ type CloudRun implements Node { """ totalFailed: Int + """ + Number of flaky tests + """ + totalFlakyTests: Int + """ This is the number of passed tests across all groups in the run """ @@ -330,6 +336,14 @@ enum CloudRunStatus { TIMEDOUT } +type CloudRunTag implements Node { + """ + Globally unique identifier representing a concrete GraphQL ObjectType + """ + id: ID! + name: String +} + """ A CloudUser represents an User stored in the Cypress Cloud """ diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 568b82d74090..89805dcf6e2c 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -217,6 +217,7 @@ type CloudRun implements Node { """Globally unique identifier representing a concrete GraphQL ObjectType""" id: ID! status: CloudRunStatus + tags: [CloudRunTag] """ Total duration of the run in milliseconds, accounting for any parallelization @@ -226,6 +227,9 @@ type CloudRun implements Node { """This is the number of failed tests across all groups in the run""" totalFailed: Int + """Number of flaky tests""" + totalFlakyTests: Int + """This is the number of passed tests across all groups in the run""" totalPassed: Int @@ -295,6 +299,12 @@ enum CloudRunStatus { TIMEDOUT } +type CloudRunTag implements Node { + """Globally unique identifier representing a concrete GraphQL ObjectType""" + id: ID! + name: String +} + """A CloudUser represents an User stored in the Cypress Cloud""" type CloudUser implements Node { """Url to manage cloud organizations for this user""" diff --git a/packages/graphql/test/stubCloudTypes.ts b/packages/graphql/test/stubCloudTypes.ts index b056d95c4243..da4b7141a87c 100644 --- a/packages/graphql/test/stubCloudTypes.ts +++ b/packages/graphql/test/stubCloudTypes.ts @@ -175,6 +175,8 @@ export function createCloudRun (config: Partial): Required { totalTests: 10, totalPassed: 10, totalDuration: 300, + totalFlakyTests: 0, + tags: [], url: 'http://dummy.cypress.io/runs/1', createdAt: new Date('1995-12-17T03:17:00').toISOString(), commitInfo: createCloudRunCommitInfo({ From a218f962230fee00cf9ac8beac2ee432035b3f43 Mon Sep 17 00:00:00 2001 From: Mark Noonan Date: Thu, 12 May 2022 13:11:00 -0400 Subject: [PATCH 16/22] feat: distinguish app vs launchpad utm_source when using utm params (#21424) --- packages/app/cypress/e2e/top-nav.cy.ts | 57 ++++++++++--------- .../src/runs/modals/CreateCloudOrgModal.vue | 2 +- .../gql-components/HeaderBarContent.cy.tsx | 22 +++---- .../topnav/PromptContent.cy.tsx | 4 +- .../src/utils/getUrlWithParams.ts | 9 +++ .../schemaTypes/objectTypes/gql-Mutation.ts | 6 +- .../launchpad/cypress/e2e/navigation.cy.ts | 2 +- packages/server/lib/gui/links.ts | 27 +-------- packages/server/test/unit/gui/links_spec.ts | 23 -------- 9 files changed, 60 insertions(+), 92 deletions(-) diff --git a/packages/app/cypress/e2e/top-nav.cy.ts b/packages/app/cypress/e2e/top-nav.cy.ts index e1178f35539e..b676381b1e38 100644 --- a/packages/app/cypress/e2e/top-nav.cy.ts +++ b/packages/app/cypress/e2e/top-nav.cy.ts @@ -264,34 +264,35 @@ describe('App Top Nav Workflows', () => { cy.findByRole('heading', { name: 'References', level: 2 }) cy.findByRole('heading', { name: 'Run in CI/CD', level: 2 }) - cy.validateExternalLink({ - name: 'Write your first test', - href: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test', - }) - - cy.validateExternalLink({ - name: 'Testing your app', - href: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App', - }) - - cy.validateExternalLink({ - name: 'Organizing Tests', - href: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests', - }) - - cy.validateExternalLink({ - name: 'Best Practices', - href: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices', - }) - - cy.validateExternalLink({ - name: 'Configuration', - href: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration', - }) - - cy.validateExternalLink({ - name: 'API', - href: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API', + const expectedLinks = [ + { + name: 'Write your first test', + href: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test&utm_source=Binary%3A+App', + }, + { + name: 'Testing your app', + href: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App&utm_source=Binary%3A+App', + }, + { + name: 'Organizing Tests', + href: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests&utm_source=Binary%3A+App', + }, + { + name: 'Best Practices', + href: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices&utm_source=Binary%3A+App', + }, + { + name: 'Configuration', + href: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration&utm_source=Binary%3A+App', + }, + { + name: 'API', + href: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API&utm_source=Binary%3A+App', + }, + ] + + expectedLinks.forEach((link) => { + cy.validateExternalLink(link) }) }) diff --git a/packages/app/src/runs/modals/CreateCloudOrgModal.vue b/packages/app/src/runs/modals/CreateCloudOrgModal.vue index 228198678ee9..664474d5de17 100644 --- a/packages/app/src/runs/modals/CreateCloudOrgModal.vue +++ b/packages/app/src/runs/modals/CreateCloudOrgModal.vue @@ -10,7 +10,7 @@ {{ t('runs.connect.modal.createOrg.description') }}

    ', { viewportWidth: 1000, viewportHeight: 750 }, ( // because outside of global mode, those are buttons that trigger popups // so this way we can assert all links at once. const expectedDocsLinks = { - [text.docsMenu.firstTest]: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test', - [text.docsMenu.testingApp]: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App', - [text.docsMenu.organizingTests]: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests', - [text.docsMenu.bestPractices]: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices', - [text.docsMenu.configuration]: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration', - [text.docsMenu.api]: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API', - [text.docsMenu.ciSetup]: 'https://on.cypress.io/ci?utm_medium=Docs+Menu&utm_content=Set+Up+CI', - [text.docsMenu.fasterTests]: 'https://on.cypress.io/parallelization?utm_medium=Docs+Menu&utm_content=Parallelization', + [text.docsMenu.firstTest]: 'https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test&utm_source=Binary%3A+Launchpad', + [text.docsMenu.testingApp]: 'https://on.cypress.io/testing-your-app?utm_medium=Docs+Menu&utm_content=Testing+Your+App&utm_source=Binary%3A+Launchpad', + [text.docsMenu.organizingTests]: 'https://on.cypress.io/writing-and-organizing-tests?utm_medium=Docs+Menu&utm_content=Organizing+Tests&utm_source=Binary%3A+Launchpad', + [text.docsMenu.bestPractices]: 'https://on.cypress.io/best-practices?utm_medium=Docs+Menu&utm_content=Best+Practices&utm_source=Binary%3A+Launchpad', + [text.docsMenu.configuration]: 'https://on.cypress.io/configuration?utm_medium=Docs+Menu&utm_content=Configuration&utm_source=Binary%3A+Launchpad', + [text.docsMenu.api]: 'https://on.cypress.io/api?utm_medium=Docs+Menu&utm_content=API&utm_source=Binary%3A+Launchpad', + [text.docsMenu.ciSetup]: 'https://on.cypress.io/ci?utm_medium=Docs+Menu&utm_content=Set+Up+CI&utm_source=Binary%3A+Launchpad', + [text.docsMenu.fasterTests]: 'https://on.cypress.io/parallelization?utm_medium=Docs+Menu&utm_content=Parallelization&utm_source=Binary%3A+Launchpad', } cy.contains('button', text.docsMenu.docsHeading).click() @@ -347,12 +347,12 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( mountWithSavedState() cy.contains( - 'a[href="https://on.cypress.io/setup-ci?utm_medium=CI+Prompt+1&utm_campaign=Other&utm_content=Automatic"]', + 'a[href="https://on.cypress.io/setup-ci?utm_medium=CI+Prompt+1&utm_campaign=Other&utm_content=Automatic&utm_source=Binary%3A+Launchpad"]', defaultMessages.topNav.docsMenu.prompts.ci1.seeOtherGuides, ).should('be.visible') cy.contains( - 'a[href="https://on.cypress.io/ci?utm_medium=CI+Prompt+1&utm_campaign=Learn+More"]', + 'a[href="https://on.cypress.io/ci?utm_medium=CI+Prompt+1&utm_campaign=Learn+More&utm_source=Binary%3A+Launchpad"]', defaultMessages.topNav.docsMenu.prompts.ci1.intro, ).should('be.visible') }) @@ -425,7 +425,7 @@ describe('', { viewportWidth: 1000, viewportHeight: 750 }, ( it('links to more information with expected utm params', () => { cy.contains( - 'a[href="https://on.cypress.io/smart-orchestration?utm_medium=CI+Prompt+1&utm_campaign=Learn+More"]', + 'a[href="https://on.cypress.io/smart-orchestration?utm_medium=CI+Prompt+1&utm_campaign=Learn+More&utm_source=Binary%3A+Launchpad"]', defaultMessages.topNav.docsMenu.prompts.orchestration1.learnMore, ) .should('be.visible') diff --git a/packages/frontend-shared/src/gql-components/topnav/PromptContent.cy.tsx b/packages/frontend-shared/src/gql-components/topnav/PromptContent.cy.tsx index 8cf0fbcf012c..ce6c1d17e028 100644 --- a/packages/frontend-shared/src/gql-components/topnav/PromptContent.cy.tsx +++ b/packages/frontend-shared/src/gql-components/topnav/PromptContent.cy.tsx @@ -27,9 +27,9 @@ describe('', { viewportWidth: 500, viewportHeight: 800 }, () => .should('have.length', 6) .eq(0) .find('a') - .should('have.attr', 'href', 'https://on.cypress.io/setup-ci-circleci?utm_medium=CI+Prompt+1&utm_campaign=Circle&utm_content=Manual') + .should('have.attr', 'href', 'https://on.cypress.io/setup-ci-circleci?utm_medium=CI+Prompt+1&utm_campaign=Circle&utm_content=Manual&utm_source=Binary%3A+Launchpad') cy.contains('a', defaultMessages.topNav.docsMenu.prompts.ci1.intro) - .should('have.attr', 'href', 'https://on.cypress.io/ci?utm_medium=CI+Prompt+1&utm_campaign=Learn+More') + .should('have.attr', 'href', 'https://on.cypress.io/ci?utm_medium=CI+Prompt+1&utm_campaign=Learn+More&utm_source=Binary%3A+Launchpad') }) }) diff --git a/packages/frontend-shared/src/utils/getUrlWithParams.ts b/packages/frontend-shared/src/utils/getUrlWithParams.ts index e0e46944fa45..0e55a9dae2c9 100644 --- a/packages/frontend-shared/src/utils/getUrlWithParams.ts +++ b/packages/frontend-shared/src/utils/getUrlWithParams.ts @@ -5,6 +5,15 @@ export type LinkWithParams = { export const getUrlWithParams = (link: LinkWithParams) => { let result = link.url + const hasUtmParams = Object.keys(link.params).some((param) => param.startsWith('utm_')) + + if (hasUtmParams) { + // __CYPRESS_MODE__ is only set on the window in th browser app - + // checking this allows us to know if links are clicked in the browser app or the launchpad + const utm_source = window.__CYPRESS_MODE__ ? 'Binary: App' : 'Binary: Launchpad' + + link.params.utm_source = utm_source + } if (link.params) { result += `?${new URLSearchParams(link.params).toString()}` diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index fb7b067b8002..9d5f44dec47b 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -93,8 +93,12 @@ export const mutation = mutationType({ resolve: (_, args, ctx) => { let url = args.url + // the `port` param is included in external links to create a cloud organization + // so that the app can be notified when the org has been created if (args.includeGraphqlPort && process.env.CYPRESS_INTERNAL_GRAPHQL_PORT) { - url = `${args.url}?port=${process.env.CYPRESS_INTERNAL_GRAPHQL_PORT}` + const joinCharacter = args.url.includes('?') ? '&' : '?' + + url = `${args.url}${joinCharacter}port=${process.env.CYPRESS_INTERNAL_GRAPHQL_PORT}` } ctx.actions.electron.openExternal(url) diff --git a/packages/launchpad/cypress/e2e/navigation.cy.ts b/packages/launchpad/cypress/e2e/navigation.cy.ts index 82ee3e321807..0566cbd86287 100644 --- a/packages/launchpad/cypress/e2e/navigation.cy.ts +++ b/packages/launchpad/cypress/e2e/navigation.cy.ts @@ -10,7 +10,7 @@ describe('Navigation', () => { cy.contains('button', defaultMessages.topNav.docsMenu.docsHeading).click() cy.contains('a', defaultMessages.topNav.docsMenu.firstTest).click() cy.wait('@OpenExternal').then((interception: Interception) => { - expect(interception.request.body.variables.url).to.equal('https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test') + expect(interception.request.body.variables.url).to.equal('https://on.cypress.io/writing-first-test?utm_medium=Docs+Menu&utm_content=First+Test&utm_source=Binary%3A+Launchpad') }) }) }) diff --git a/packages/server/lib/gui/links.ts b/packages/server/lib/gui/links.ts index 265d771a0e31..782c51c55f0f 100644 --- a/packages/server/lib/gui/links.ts +++ b/packages/server/lib/gui/links.ts @@ -1,29 +1,6 @@ -import _ from 'lodash' -import { URL, URLSearchParams } from 'url' - // NOTE: in order for query params to be passed through on links // forwardQueryParams: true must be set for that slug in the on package -interface OpenExternalOptions { - url: string - params: { [key: string]: string } -} - -export const openExternal = (opts: OpenExternalOptions | string) => { - if (_.isString(opts)) { - return require('electron').shell.openExternal(opts) - } - - const url = new URL(opts.url) - - if (opts.params) { - // just add the utm_source here so we don't have to duplicate it on every link - if (_.find(opts.params, (_val, key) => _.includes(key, 'utm_'))) { - opts.params.utm_source = 'Test Runner' - } - - url.search = new URLSearchParams(opts.params).toString() - } - - return require('electron').shell.openExternal(url.href) +export const openExternal = (url: string) => { + return require('electron').shell.openExternal(url) } diff --git a/packages/server/test/unit/gui/links_spec.ts b/packages/server/test/unit/gui/links_spec.ts index 0abfc8481156..b4e5d39510d8 100644 --- a/packages/server/test/unit/gui/links_spec.ts +++ b/packages/server/test/unit/gui/links_spec.ts @@ -20,27 +20,4 @@ describe('lib/gui/links', () => { openExternal('https://on.cypress.io/string-link') expect(shell.openExternal).to.be.calledWith('https://on.cypress.io/string-link') }) - - it('appends get parameters', () => { - openExternal({ - url: 'https://on.cypress.io/string-link', - params: { - search: 'term', - }, - }) - - expect(shell.openExternal).to.be.calledWith('https://on.cypress.io/string-link?search=term') - }) - - it('automatically adds utm_source if utm params are present', () => { - openExternal({ - url: 'https://on.cypress.io/string-link', - params: { - utm_medium: 'GUI Tab', - utm_campaign: 'Learn More', - }, - }) - - expect(shell.openExternal).to.be.calledWith('https://on.cypress.io/string-link?utm_medium=GUI+Tab&utm_campaign=Learn+More&utm_source=Test+Runner') - }) }) From fec27a0be952c91b96f3b9b5efd5fe7488312fc4 Mon Sep 17 00:00:00 2001 From: Mark Noonan Date: Thu, 12 May 2022 14:49:44 -0400 Subject: [PATCH 17/22] fix: test-recording instructions in Component Test mode (#21422) --- packages/app/cypress/e2e/runs.cy.ts | 74 +++++++++++-------- packages/app/src/runs/RunsEmpty.vue | 5 +- .../src/components/TerminalPrompt.cy.tsx | 6 +- .../src/components/TerminalPrompt.vue | 3 +- 4 files changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index 0dda1323a40c..6a1da9906917 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -1,6 +1,27 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' import type { SinonStub } from 'sinon' +function scaffoldTestingTypeAndVisitRunsPage (testingType: 'e2e' | 'component') { + cy.scaffoldProject('cypress-in-cypress') + cy.openProject('cypress-in-cypress') + cy.startAppServer(testingType) + + cy.loginUser() + + // make sure there are no runs found for the project ID + cy.remoteGraphQLIntercept(async (obj) => { + if (obj.result.data?.cloudProjectBySlug) { + obj.result.data.cloudProjectBySlug.runs.nodes = [] + } + + return obj.result + }) + + cy.visitApp() + + return cy.get('[href="#/runs"]').click() +} + describe('App: Runs', { viewportWidth: 1200 }, () => { context('Runs Page', () => { beforeEach(() => { @@ -398,47 +419,40 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.findByText(defaultMessages.runs.connect.buttonProject).should('exist') }) - it('displays how to record prompt when connected and no runs', () => { - cy.scaffoldProject('component-tests') - cy.openProject('component-tests') - cy.startAppServer('component') - - cy.loginUser() - cy.remoteGraphQLIntercept(async (obj) => { - if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) { - obj.result.data.cloudProjectBySlug.runs.nodes = [] - } + it('displays how to record prompt when connected and no runs in Component Testing', () => { + scaffoldTestingTypeAndVisitRunsPage('component') + cy.contains(defaultMessages.runs.empty.title).should('be.visible') + cy.contains(defaultMessages.runs.empty.description).should('be.visible') + cy.contains('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible') + }) - return obj.result - }) + it('displays how to record prompt when connected and no runs in E2E', () => { + scaffoldTestingTypeAndVisitRunsPage('e2e') - cy.visitApp() - cy.get('[href="#/runs"]').click() - cy.contains(defaultMessages.runs.empty.title) - cy.contains(defaultMessages.runs.empty.description) - cy.contains('--record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') + cy.contains(defaultMessages.runs.empty.title).should('be.visible') + cy.contains(defaultMessages.runs.empty.description).should('be.visible') + cy.contains('cypress run --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa').should('be.visible') }) - it('displays a copy button', () => { - cy.scaffoldProject('component-tests') - cy.openProject('component-tests') - cy.startAppServer('component') - + it('displays a copy button and copies correct command in Component Testing', () => { + scaffoldTestingTypeAndVisitRunsPage('component') cy.withCtx(async (ctx, o) => { o.sinon.stub(ctx.electronApi, 'copyTextToClipboard') }) - cy.loginUser() - cy.remoteGraphQLIntercept(async (obj) => { - if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) { - obj.result.data.cloudProjectBySlug.runs.nodes = [] - } + cy.get('[data-cy="copy-button"]').click() + cy.contains('Copied!') + cy.withRetryableCtx((ctx) => { + expect(ctx.electronApi.copyTextToClipboard as SinonStub).to.have.been.calledWith('cypress run --component --record --key 2aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') + }) + }) - return obj.result + it('displays a copy button and copies correct command in E2E', () => { + scaffoldTestingTypeAndVisitRunsPage('e2e') + cy.withCtx(async (ctx, o) => { + o.sinon.stub(ctx.electronApi, 'copyTextToClipboard') }) - cy.visitApp() - cy.get('[href="#/runs"]').click() cy.get('[data-cy="copy-button"]').click() cy.contains('Copied!') cy.withRetryableCtx((ctx) => { diff --git a/packages/app/src/runs/RunsEmpty.vue b/packages/app/src/runs/RunsEmpty.vue index d1dac4553d7c..318079792135 100644 --- a/packages/app/src/runs/RunsEmpty.vue +++ b/packages/app/src/runs/RunsEmpty.vue @@ -33,6 +33,7 @@ fragment RunsEmpty on CurrentProject { title projectId configFile + currentTestingType cloudProject { __typename ... on CloudProject { @@ -58,7 +59,9 @@ const firstRecordKey = computed(() => { : '' }) const recordCommand = computed(() => { - return `cypress run --record --key ${firstRecordKey.value}` + const componentFlagOrSpace = props.gql.currentTestingType === 'component' ? ' --component ' : ' ' + + return `cypress run${componentFlagOrSpace}--record --key ${firstRecordKey.value}` }) diff --git a/packages/frontend-shared/src/components/TerminalPrompt.cy.tsx b/packages/frontend-shared/src/components/TerminalPrompt.cy.tsx index c7948b6343a5..e68b7ed53905 100644 --- a/packages/frontend-shared/src/components/TerminalPrompt.cy.tsx +++ b/packages/frontend-shared/src/components/TerminalPrompt.cy.tsx @@ -5,7 +5,7 @@ describe('', () => { it('renders without overflow', { viewportWidth: 800, viewportHeight: 120 }, () => { cy.mount(() => (
    - +
    )) @@ -16,16 +16,14 @@ describe('', () => { it('overflows nicely', { viewportWidth: 800, viewportHeight: 120 }, () => { const command = 'yarn workspace @packages/frontend-shared cypress:run --record --key 123as4d56asda987das' - const projectFolderName = 'design-system' cy.mount(() => (
    - +
    )) cy.contains(command) - cy.contains(projectFolderName) cy.contains('button', defaultMessages.clipboard.copy) .should('be.visible') .percySnapshot() diff --git a/packages/frontend-shared/src/components/TerminalPrompt.vue b/packages/frontend-shared/src/components/TerminalPrompt.vue index aec6e638e81a..e9ff97883a5c 100644 --- a/packages/frontend-shared/src/components/TerminalPrompt.vue +++ b/packages/frontend-shared/src/components/TerminalPrompt.vue @@ -2,7 +2,7 @@ - {{ projectFolderName }}$ + $ {{ command }}
    @@ -17,7 +17,6 @@ import CopyButton from '../gql-components/CopyButton.vue' defineProps<{ - projectFolderName?: string command: string }>() From 123a930831922450fab6f992f9634a7dff28c63e Mon Sep 17 00:00:00 2001 From: Alejandro Estrada Date: Thu, 12 May 2022 14:30:03 -0500 Subject: [PATCH 18/22] fix: remove breaking options from testing type on migration (#21437) * Add failing test * fix test * Refactor code * Update test --- .../data-context/__snapshots__/codegen.spec.ts.js | 1 - .../data-context/src/sources/migration/codegen.ts | 14 ++++++++++---- .../test/unit/sources/migration/codegen.spec.ts | 3 ++- .../migration-component-testing/cypress.json | 3 ++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/data-context/__snapshots__/codegen.spec.ts.js b/packages/data-context/__snapshots__/codegen.spec.ts.js index b4548acacfd6..ec8c9a31947f 100644 --- a/packages/data-context/__snapshots__/codegen.spec.ts.js +++ b/packages/data-context/__snapshots__/codegen.spec.ts.js @@ -229,7 +229,6 @@ const { defineConfig } = require('cypress') module.exports = defineConfig({ component: { setupNodeEvents(on, config) {}, - componentFolder: '.', specPattern: './**/*.spec.cy.{js,ts,jsx,tsx}', }, }) diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts index 27130c0e393d..7033103aa571 100644 --- a/packages/data-context/src/sources/migration/codegen.ts +++ b/packages/data-context/src/sources/migration/codegen.ts @@ -14,6 +14,8 @@ import { hasDefaultExport } from './parserUtils' import type { LegacyCypressConfigJson } from '..' import { parse } from '@babel/parser' import generate from '@babel/generator' +import _ from 'lodash' +import { getBreakingKeys } from '@packages/config' const debug = Debug('cypress:data-context:sources:migration:codegen') @@ -383,19 +385,23 @@ export function reduceConfig (cfg: LegacyCypressConfigJson, options: CreateConfi const isDefaultE2E = key === 'e2e' && specPattern === `cypress/e2e/${ext}` const isDefaultCT = key === 'component' && specPattern === ext + const breakingKeys = getBreakingKeys() + const restWithoutBreakingKeys = _.omit(rest, breakingKeys) + const existingWithoutBreakingKeys = _.omit(acc[key], breakingKeys) + if (isDefaultE2E || isDefaultCT) { return { ...acc, [key]: { - ...rest, - ...acc[key], + ...restWithoutBreakingKeys, + ...existingWithoutBreakingKeys, }, } } return { ...acc, [key]: { - ...rest, - ...acc[key], + ...restWithoutBreakingKeys, + ...existingWithoutBreakingKeys, specPattern, }, } diff --git a/packages/data-context/test/unit/sources/migration/codegen.spec.ts b/packages/data-context/test/unit/sources/migration/codegen.spec.ts index 09aaf087cd5a..61b68ec2f38b 100644 --- a/packages/data-context/test/unit/sources/migration/codegen.spec.ts +++ b/packages/data-context/test/unit/sources/migration/codegen.spec.ts @@ -415,7 +415,7 @@ describe('reduceConfig', () => { hasE2ESpec: false, hasPluginsFile: false, projectRoot: '', - hasTypescript: false, + isUsingTypeScript: false, isProjectUsingESModules: false, shouldAddCustomE2ESpecPattern: false, } @@ -448,6 +448,7 @@ describe('reduceConfig', () => { } const newConfig = reduceConfig(config, options) + expect(newConfig.component.componentFolder).to.not.exist expect(newConfig.component.specPattern).to.eq('src/**/**.cy.js') expect(newConfig.e2e.specPattern).to.eq(`${config.e2e.integrationFolder}/${config.testFiles}`) }) diff --git a/system-tests/projects/migration-component-testing/cypress.json b/system-tests/projects/migration-component-testing/cypress.json index 77e8c07980c3..90d2979097b7 100644 --- a/system-tests/projects/migration-component-testing/cypress.json +++ b/system-tests/projects/migration-component-testing/cypress.json @@ -2,6 +2,7 @@ "componentFolder": "src", "supportFile": false, "component": { - "testFiles": "**/*spec.{js,tsx}" + "testFiles": "**/*spec.{js,tsx}", + "componentFolder": "src" } } \ No newline at end of file From 07478cb7a6c00f7a1b1a06918f6d1af7c293b397 Mon Sep 17 00:00:00 2001 From: Alejandro Estrada Date: Thu, 12 May 2022 15:55:46 -0500 Subject: [PATCH 19/22] fix: use correct path for scaffolding spec on CT (#21411) * fix: use correct path for scaffolding spec on CT * Refactor code * Update with feedback --- packages/app/cypress/e2e/specs.cy.ts | 6 +++ .../src/sources/ProjectDataSource.ts | 18 +++++--- .../unit/sources/ProjectDataSource.spec.ts | 44 +++++++++---------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 08e331ceb09d..d2848915db0a 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -509,6 +509,8 @@ describe('App: Specs', () => { it('shows success modal when empty spec is created', () => { cy.get('@CreateEmptySpecDialog').within(() => { + cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts')) + cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js') cy.findByRole('button', { name: 'Create Spec' }).click() @@ -533,6 +535,8 @@ describe('App: Specs', () => { it('navigates to spec runner when selected', () => { cy.get('@CreateEmptySpecDialog').within(() => { + cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts')) + cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js') cy.findByRole('button', { name: 'Create Spec' }).click() @@ -551,6 +555,8 @@ describe('App: Specs', () => { it('displays alert with docs link on new spec', () => { cy.get('@CreateEmptySpecDialog').within(() => { + cy.findByLabelText('Enter a relative path...').invoke('val').should('eq', getPathForPlatform('cypress/component/filename.cy.ts')) + cy.findByLabelText('Enter a relative path...').clear().type('cypress/my-empty-spec.cy.js') cy.findByRole('button', { name: 'Create Spec' }).click() diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts index fb842bdd6059..f44803205557 100644 --- a/packages/data-context/src/sources/ProjectDataSource.ts +++ b/packages/data-context/src/sources/ProjectDataSource.ts @@ -1,6 +1,6 @@ import os from 'os' import chokidar from 'chokidar' -import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec } from '@packages/types' +import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec, TestingType } from '@packages/types' import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' import { scanFSForAvailableDependency } from 'create-cypress-tests' import minimatch from 'minimatch' @@ -104,7 +104,7 @@ export function transformSpec ({ } } -export function getDefaultSpecFileName (specPattern: string, fileExtensionToUse?: 'js' | 'ts') { +export function getDefaultSpecFileName (specPattern: string, testingType: TestingType, fileExtensionToUse?: 'js' | 'ts') { function replaceWildCard (s: string, fallback: string) { return s.replace(/\*/g, fallback) } @@ -121,7 +121,7 @@ export function getDefaultSpecFileName (specPattern: string, fileExtensionToUse? dirname = dirname.replace('**', 'cypress') } - const splittedDirname = dirname.split('/').filter((s) => s !== '**').map((x) => replaceWildCard(x, 'e2e')).join('/') + const splittedDirname = dirname.split('/').filter((s) => s !== '**').map((x) => replaceWildCard(x, testingType)).join('/') const fileName = replaceWildCard(parsedGlob.path.filename, 'filename') const extnameWithoutExt = parsedGlob.path.extname.replace(parsedGlob.path.ext, '') @@ -282,13 +282,15 @@ export class ProjectDataSource { } async defaultSpecFileName () { - const defaultFileName = 'cypress/e2e/filename.cy.js' + const getDefaultFileName = (testingType: TestingType) => `cypress/${testingType}/filename.cy.${this.ctx.lifecycleManager.fileExtensionToUse}` try { if (!this.ctx.currentProject || !this.ctx.coreData.currentTestingType) { return null } + const defaultFileName = getDefaultFileName(this.ctx.coreData.currentTestingType) + let specPatternSet: string | undefined const { specPattern = [] } = await this.ctx.project.specPatterns() @@ -300,7 +302,11 @@ export class ProjectDataSource { return defaultFileName } - const specFileName = getDefaultSpecFileName(specPatternSet, this.ctx.lifecycleManager.fileExtensionToUse) + if (specPatternSet === defaultSpecPattern[this.ctx.coreData.currentTestingType]) { + return defaultFileName + } + + const specFileName = getDefaultSpecFileName(specPatternSet, this.ctx.coreData.currentTestingType, this.ctx.lifecycleManager.fileExtensionToUse) if (!specFileName) { return defaultFileName @@ -308,7 +314,7 @@ export class ProjectDataSource { return specFileName } catch { - return defaultFileName + return getDefaultFileName(this.ctx.coreData.currentTestingType ?? 'e2e') } } diff --git a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts index f393ae74adbb..2a864587227a 100644 --- a/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts +++ b/packages/data-context/test/unit/sources/ProjectDataSource.spec.ts @@ -192,31 +192,31 @@ describe('getDefaultSpecFileName', () => { context('dirname', () => { it('returns pattern without change if it is do not a glob', () => { const specPattern = 'cypress/e2e/foo.spec.ts' - const defaultFileName = getDefaultSpecFileName(specPattern) + const defaultFileName = getDefaultSpecFileName(specPattern, 'e2e') expect(defaultFileName).to.eq(specPattern) }) it('remove ** from glob if it is not in the beginning', () => { - const defaultFileName = getDefaultSpecFileName('cypress/**/foo.spec.ts') + const defaultFileName = getDefaultSpecFileName('cypress/**/foo.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/foo.spec.ts') }) it('replace ** for cypress if it starts with **', () => { - const defaultFileName = getDefaultSpecFileName('**/e2e/foo.spec.ts') + const defaultFileName = getDefaultSpecFileName('**/e2e/foo.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts') }) it('replace ** for cypress if it starts with ** and omit extra **', () => { - const defaultFileName = getDefaultSpecFileName('**/**/foo.spec.ts') + const defaultFileName = getDefaultSpecFileName('**/**/foo.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/foo.spec.ts') }) it('selects first option if there are multiples possibilities of values', () => { - const defaultFileName = getDefaultSpecFileName('{cypress,tests}/{integration,e2e}/foo.spec.ts') + const defaultFileName = getDefaultSpecFileName('{cypress,tests}/{integration,e2e}/foo.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/integration/foo.spec.ts') }) @@ -224,13 +224,13 @@ describe('getDefaultSpecFileName', () => { context('filename', () => { it('replace * for filename', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/*.spec.ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/*.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts') }) it('selects first option if there are multiples possibilities of values', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/{foo,filename}.spec.ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/{foo,filename}.spec.ts', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts') }) @@ -238,13 +238,13 @@ describe('getDefaultSpecFileName', () => { context('test extension', () => { it('replace * for filename', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.*.ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.*.ts', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts') }) it('selects first option if there are multiples possibilities of values', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.{spec,cy}.ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.{spec,cy}.ts', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts') }) @@ -252,25 +252,25 @@ describe('getDefaultSpecFileName', () => { context('lang extension', () => { it('if project use TS, set TS as extension if it exists in the glob', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.ts', 'ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.ts', 'e2e', 'ts') expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts') }) it('if project use TS, set TS as extension if it exists in the options of extensions', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,ts,tsx}', 'ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,ts,tsx}', 'e2e', 'ts') expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts') }) it('if project use TS, do not set TS as extension if it do not exists in the options of extensions', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,jsx}', 'ts') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{js,jsx}', 'e2e', 'ts') expect(defaultFileName).to.eq('cypress/e2e/filename.cy.js') }) it('selects first option if there are multiples possibilities of values', () => { - const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{ts,js}') + const defaultFileName = getDefaultSpecFileName('cypress/e2e/filename.cy.{ts,js}', 'e2e') expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts') }) @@ -278,43 +278,43 @@ describe('getDefaultSpecFileName', () => { context('extra cases', () => { it('creates specName for tests/*.js', () => { - const defaultFileName = getDefaultSpecFileName('tests/*.js') + const defaultFileName = getDefaultSpecFileName('tests/*.js', 'e2e') expect(defaultFileName).to.eq('tests/filename.js') }) it('creates specName for src/*-test.js', () => { - const defaultFileName = getDefaultSpecFileName('src/*-test.js') + const defaultFileName = getDefaultSpecFileName('src/*-test.js', 'e2e') expect(defaultFileName).to.eq('src/filename-test.js') }) it('creates specName for src/*.foo.bar.js', () => { - const defaultFileName = getDefaultSpecFileName('src/*.foo.bar.js') + const defaultFileName = getDefaultSpecFileName('src/*.foo.bar.js', 'e2e') expect(defaultFileName).to.eq('src/filename.foo.bar.js') }) it('creates specName for src/prefix.*.test.js', () => { - const defaultFileName = getDefaultSpecFileName('src/prefix.*.test.js') + const defaultFileName = getDefaultSpecFileName('src/prefix.*.test.js', 'e2e') expect(defaultFileName).to.eq('src/prefix.cy.test.js') }) it('creates specName for src/*/*.test.js', () => { - const defaultFileName = getDefaultSpecFileName('src/*/*.test.js') + const defaultFileName = getDefaultSpecFileName('src/*/*.test.js', 'e2e') expect(defaultFileName).to.eq('src/e2e/filename.test.js') }) it('creates specName for src-*/**/*.test.js', () => { - const defaultFileName = getDefaultSpecFileName('src-*/**/*.test.js') + const defaultFileName = getDefaultSpecFileName('src-*/**/*.test.js', 'e2e') expect(defaultFileName).to.eq('src-e2e/filename.test.js') }) it('creates specName for src/*.test.(js|jsx)', () => { - const defaultFileName = getDefaultSpecFileName('src/*.test.(js|jsx)') + const defaultFileName = getDefaultSpecFileName('src/*.test.(js|jsx)', 'e2e') const possiblesFileNames = ['src/filename.test.jsx', 'src/filename.test.js'] @@ -322,7 +322,7 @@ describe('getDefaultSpecFileName', () => { }) it('creates specName for (src|components)/**/*.test.js', () => { - const defaultFileName = getDefaultSpecFileName('(src|components)/**/*.test.js') + const defaultFileName = getDefaultSpecFileName('(src|components)/**/*.test.js', 'e2e') const possiblesFileNames = ['src/filename.test.js', 'components/filename.test.js'] @@ -330,7 +330,7 @@ describe('getDefaultSpecFileName', () => { }) it('creates specName for e2e/**/*.cy.{js,jsx,ts,tsx}', () => { - const defaultFileName = getDefaultSpecFileName('e2e/**/*.cy.{js,jsx,ts,tsx}') + const defaultFileName = getDefaultSpecFileName('e2e/**/*.cy.{js,jsx,ts,tsx}', 'e2e') expect(defaultFileName).to.eq('e2e/filename.cy.js') }) From df3ca1524d0b2f2d7cd48b0140d2022b64133aa3 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Thu, 12 May 2022 17:45:25 -0500 Subject: [PATCH 20/22] chore: remove unused codeGenGlobs (#21438) --- packages/app/src/specs/CreateSpecModal.cy.tsx | 10 ------- packages/app/src/specs/CreateSpecModal.vue | 13 --------- packages/data-context/package.json | 1 - .../src/sources/ProjectDataSource.ts | 27 ------------------- .../test/unit/codegen/code-generator.spec.ts | 8 ------ .../support/mock-graphql/stubgql-Project.ts | 5 ---- packages/graphql/schemas/schema.graphql | 9 ------- .../objectTypes/gql-CodeGenGlobs.ts | 10 ------- .../objectTypes/gql-CurrentProject.ts | 6 ----- .../src/schemaTypes/objectTypes/index.ts | 1 - packages/scaffold-config/src/frameworks.ts | 8 ------ 11 files changed, 98 deletions(-) delete mode 100644 packages/graphql/src/schemaTypes/objectTypes/gql-CodeGenGlobs.ts diff --git a/packages/app/src/specs/CreateSpecModal.cy.tsx b/packages/app/src/specs/CreateSpecModal.cy.tsx index bfb16f7da59e..fd8d2dbc5368 100644 --- a/packages/app/src/specs/CreateSpecModal.cy.tsx +++ b/packages/app/src/specs/CreateSpecModal.cy.tsx @@ -14,11 +14,6 @@ describe('', () => { gql={{ currentProject: { id: 'id', - codeGenGlobs: { - id: 'super-unique-id', - __typename: 'CodeGenGlobs', - component: '**.vue', - }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', @@ -79,11 +74,6 @@ describe('playground', () => { gql={{ currentProject: { id: 'id', - codeGenGlobs: { - id: 'super-unique-id', - __typename: 'CodeGenGlobs', - component: '**.vue', - }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', diff --git a/packages/app/src/specs/CreateSpecModal.vue b/packages/app/src/specs/CreateSpecModal.vue index 5ec9bf3ae1f2..e2fad62993fa 100644 --- a/packages/app/src/specs/CreateSpecModal.vue +++ b/packages/app/src/specs/CreateSpecModal.vue @@ -20,7 +20,6 @@ v-if="generator" :key="`${generator.id}-${iteration}`" v-model:title="title" - :code-gen-glob="codeGenGlob" :gql="props.gql.currentProject" :type="props.gql.currentProject?.currentTestingType" :spec-file-name="specFileName" @@ -78,10 +77,6 @@ fragment CreateSpecModal on Query { id fileExtensionToUse defaultSpecFileName - codeGenGlobs { - id - component - } ...EmptyGenerator } } @@ -114,14 +109,6 @@ const specFileName = computed(() => { return getPathForPlatform(fileName) }) -const codeGenGlob = computed(() => { - if (!generator.value) { - return null - } - - return props.gql.currentProject?.codeGenGlobs[generator.value.id] -}) - const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject?.currentTestingType) const singleGenerator = computed(() => filteredGenerators.value.length === 1 ? filteredGenerators.value[0] : null) diff --git a/packages/data-context/package.json b/packages/data-context/package.json index 2b4cf1fc0d45..51c396b6ee0f 100644 --- a/packages/data-context/package.json +++ b/packages/data-context/package.json @@ -23,7 +23,6 @@ "@urql/exchange-graphcache": "4.3.6", "chokidar": "3.5.1", "common-path-prefix": "3.0.0", - "create-cypress-tests": "0.0.0-development", "cross-fetch": "^3.1.4", "dataloader": "^2.0.0", "dayjs": "^1.9.3", diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts index f44803205557..84dea93e5a1e 100644 --- a/packages/data-context/src/sources/ProjectDataSource.ts +++ b/packages/data-context/src/sources/ProjectDataSource.ts @@ -1,8 +1,6 @@ import os from 'os' import chokidar from 'chokidar' import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec, TestingType } from '@packages/types' -import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' -import { scanFSForAvailableDependency } from 'create-cypress-tests' import minimatch from 'minimatch' import { debounce, isEqual } from 'lodash' import path from 'path' @@ -365,31 +363,6 @@ export class ProjectDataSource { return preferences[projectTitle] ?? null } - private guessFramework (projectRoot: string) { - const guess = WIZARD_FRAMEWORKS.find((framework) => { - const lookingForDeps = framework.detectors.map((x) => x.package).reduce( - (acc, dep) => ({ ...acc, [dep]: '*' }), - {}, - ) - - return scanFSForAvailableDependency(projectRoot, lookingForDeps) - }) - - return guess ?? null - } - - async getCodeGenGlobs () { - assert(this.ctx.currentProject, `Cannot find glob without currentProject.`) - - const looseComponentGlob = '*.{js,jsx,ts,tsx,.vue}' - - const framework = this.guessFramework(this.ctx.currentProject) - - return { - component: framework?.glob ?? looseComponentGlob, - } - } - async getResolvedConfigFields (): Promise { const config = this.ctx.lifecycleManager.loadedFullConfig?.resolved ?? {} diff --git a/packages/data-context/test/unit/codegen/code-generator.spec.ts b/packages/data-context/test/unit/codegen/code-generator.spec.ts index 9f80a4218725..393ebae41b4c 100644 --- a/packages/data-context/test/unit/codegen/code-generator.spec.ts +++ b/packages/data-context/test/unit/codegen/code-generator.spec.ts @@ -1,10 +1,8 @@ import { parse } from '@babel/parser' -import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' import { expect } from 'chai' import dedent from 'dedent' import fs from 'fs-extra' import path from 'path' -import sinon from 'sinon' import { DataContext } from '../../../src' import { Action, codeGenerator, CodeGenResult, CodeGenResults, @@ -221,9 +219,6 @@ describe('code-generator', () => { target, } - // @ts-ignore - sinon.stub(ctx.project, 'guessFramework').returns(WIZARD_FRAMEWORKS[0]) - const newSpecCodeGenOptions = new SpecOptions(ctx, { codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'), codeGenType: 'component', @@ -244,9 +239,6 @@ describe('code-generator', () => { target, } - // @ts-ignore - sinon.stub(ctx.project, 'guessFramework').returns(WIZARD_FRAMEWORKS[1]) - const newSpecCodeGenOptions = new SpecOptions(ctx, { codeGenPath: path.join(__dirname, 'files', 'vue', 'Button.vue'), codeGenType: 'component', diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts index 7d0c18a24a2e..555a5e621506 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts @@ -43,11 +43,6 @@ export const createTestCurrentProject = (title: string, currentProject: Partial< ], config, cloudProject: CloudProjectStubs.componentProject, - codeGenGlobs: { - id: 'super-unique-id', - __typename: 'CodeGenGlobs', - component: '**/*.vue', - }, activeBrowser: stubBrowsers[0], browsers: stubBrowsers, isDefaultSpecPattern: true, diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 89805dcf6e2c..46224a03647a 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -357,14 +357,6 @@ type CodeFrame { line: Int } -"""Glob patterns for detecting files for code gen.""" -type CodeGenGlobs implements Node { - component: String! - - """Relay style Node ID field for the CodeGenGlobs field""" - id: ID! -} - enum CodeGenType { component e2e @@ -397,7 +389,6 @@ type CurrentProject implements Node & ProjectLike { """List of all code generation candidates stories""" codeGenCandidates(glob: String!): [FileParts] - codeGenGlobs: CodeGenGlobs! """Project configuration""" config: JSON! diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-CodeGenGlobs.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-CodeGenGlobs.ts deleted file mode 100644 index 135e21d8c66d..000000000000 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-CodeGenGlobs.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { objectType } from 'nexus' - -export const CodeGenGlobs = objectType({ - name: 'CodeGenGlobs', - description: 'Glob patterns for detecting files for code gen.', - node: 'component', - definition (t) { - t.nonNull.string('component') - }, -}) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts index 6a854147bc9a..4bf20c37f27b 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts @@ -4,7 +4,6 @@ import path from 'path' import { BrowserStatusEnum, FileExtensionEnum } from '..' import { TestingTypeEnum } from '../enumTypes/gql-WizardEnums' import { Browser } from './gql-Browser' -import { CodeGenGlobs } from './gql-CodeGenGlobs' import { FileParts } from './gql-FileParts' import { ProjectPreferences } from './gql-ProjectPreferences' import { Spec } from './gql-Spec' @@ -194,11 +193,6 @@ export const CurrentProject = objectType({ }, }) - t.nonNull.field('codeGenGlobs', { - type: CodeGenGlobs, - resolve: (src, args, ctx) => ctx.project.getCodeGenGlobs(), - }) - t.list.field('codeGenCandidates', { type: FileParts, description: 'List of all code generation candidates stories', diff --git a/packages/graphql/src/schemaTypes/objectTypes/index.ts b/packages/graphql/src/schemaTypes/objectTypes/index.ts index 9e71622a9eb2..2333c7664d58 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/index.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/index.ts @@ -5,7 +5,6 @@ export * from './gql-AuthState' export * from './gql-Browser' export * from './gql-CachedUser' export * from './gql-CodeFrame' -export * from './gql-CodeGenGlobs' export * from './gql-CurrentProject' export * from './gql-DevState' export * from './gql-Editor' diff --git a/packages/scaffold-config/src/frameworks.ts b/packages/scaffold-config/src/frameworks.ts index b8ebccf9b710..b44860d7ea2a 100644 --- a/packages/scaffold-config/src/frameworks.ts +++ b/packages/scaffold-config/src/frameworks.ts @@ -73,7 +73,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'react', - glob: '*.{js,jsx,tsx}', mountModule: 'cypress/react', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), @@ -93,7 +92,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'vue', - glob: '*.vue', mountModule: 'cypress/vue2', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), @@ -113,7 +111,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'vue', - glob: '*.vue', mountModule: 'cypress/vue', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), @@ -132,7 +129,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'react', - glob: '*.{js,jsx,tsx}', mountModule: 'cypress/react', supportStatus: 'alpha', componentIndexHtml: componentIndexHtmlGenerator('
    '), @@ -151,7 +147,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'vue', - glob: '*.vue', mountModule: 'cypress/vue2', supportStatus: 'alpha', componentIndexHtml: componentIndexHtmlGenerator(), @@ -170,7 +165,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'vue', - glob: '*.vue', mountModule: 'cypress/vue2', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), @@ -189,7 +183,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'vue', - glob: '*.vue', mountModule: 'cypress/vue', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), @@ -208,7 +201,6 @@ export const WIZARD_FRAMEWORKS = [ ] }, codeGenFramework: 'react', - glob: '*.{js,jsx,tsx}', mountModule: 'cypress/react', supportStatus: 'full', componentIndexHtml: componentIndexHtmlGenerator(), From 3a3aa10630f0530eca8adebc428e224d64f67b32 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Thu, 12 May 2022 18:45:29 -0500 Subject: [PATCH 21/22] test: fix flaky cy-in-cy selector validity test (#21360) --- packages/app/cypress/e2e/cypress-in-cypress.cy.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts index aaf312db80b8..64eda51c6b43 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress.cy.ts @@ -208,8 +208,10 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100 it(`resets selector playground validity when selecting element with playground selector in ${testingType}`, () => { startAtSpecsPage(testingType) - cy.get('[data-cy="spec-item"]').first().click() - cy.get('#unified-reporter').should('be.visible') + const spec = testingType === 'e2e' ? 'dom-content.spec.js' : 'TestComponent.spec.jsx' + + cy.get('[data-cy="spec-item"]').contains(spec).click() + cy.get('.passed > .num').should('contain', 1) cy.get('[data-cy="playground-activator"]').click() cy.get('[data-cy="playground-selector"]').clear() From dcb4a61b4e522c676e8078732634972fe64afe5f Mon Sep 17 00:00:00 2001 From: Alejandro Estrada Date: Thu, 12 May 2022 21:48:12 -0500 Subject: [PATCH 22/22] fix: migrate multiples projects when in global mode (#21458) * fix: migrate multiples projects when in global mode * Add test * Update test --- .../src/actions/MigrationActions.ts | 8 ++-- .../launchpad/cypress/e2e/migration.cy.ts | 45 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/data-context/src/actions/MigrationActions.ts b/packages/data-context/src/actions/MigrationActions.ts index 486aee8281f5..0cb1096a3565 100644 --- a/packages/data-context/src/actions/MigrationActions.ts +++ b/packages/data-context/src/actions/MigrationActions.ts @@ -260,9 +260,11 @@ export class MigrationActions { throw error }) - // @ts-ignore configFile needs to be updated with the new one, so it finds the correct one - // with the new file, instead of the deleted one which is not supported anymore - this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration + if (this.ctx.modeOptions.configFile) { + // @ts-ignore configFile needs to be updated with the new one, so it finds the correct one + // with the new file, instead of the deleted one which is not supported anymore + this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration + } } async setLegacyConfigForMigration (config: LegacyCypressConfigJson) { diff --git a/packages/launchpad/cypress/e2e/migration.cy.ts b/packages/launchpad/cypress/e2e/migration.cy.ts index b275778891d5..e1342fae16c6 100644 --- a/packages/launchpad/cypress/e2e/migration.cy.ts +++ b/packages/launchpad/cypress/e2e/migration.cy.ts @@ -78,6 +78,51 @@ function renameSupport (lang: 'js' | 'ts' | 'coffee' = 'js') { }, { lang }) } +describe('global mode', () => { + it('migrates 2 projects in global mode', () => { + cy.openGlobalMode() + cy.addProject('migration-e2e-export-default') + cy.addProject('migration-e2e-custom-integration-with-projectId') + cy.visitLaunchpad() + + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.actions.migration, 'locallyInstalledCypressVersion').returns('10.0.0') + }) + + cy.contains('migration-e2e-export-default').click() + // rename integration->e2e + cy.get(renameAutoStep).should('exist') + cy.get(renameManualStep).should('not.exist') + + // cypress/support/index.ts -> cypress/support/e2e.ts + cy.get(renameSupportStep).should('exist') + // no component specs + cy.get(setupComponentStep).should('not.exist') + + cy.get(configFileStep).should('exist') + + runAutoRename() + renameSupport('ts') + migrateAndVerifyConfig('cypress.config.ts') + checkOutcome() + + cy.contains('Projects').click() + cy.contains('migration-e2e-custom-integration-with-projectId').click() + // default testFiles but custom integration - can rename automatically + cy.get(renameAutoStep).should('not.exist') + // no CT + cy.get(renameManualStep).should('not.exist') + // supportFile is false - cannot migrate + cy.get(renameSupportStep).should('exist') + cy.get(setupComponentStep).should('not.exist') + cy.get(configFileStep).should('exist') + + renameSupport() + migrateAndVerifyConfig() + checkOutcome() + }) +}) + describe('Opening unmigrated project', () => { it('legacy project with --e2e', () => { cy.scaffoldProject('migration')