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 @@
+
+
+
', { 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')