mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-05 22:40:49 -05:00
Merge pull request #20775 from cypress-io/ryanm/feat/refactor-config-loading
This commit is contained in:
@@ -2,13 +2,13 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json'
|
||||
import type { SinonStub } from 'sinon'
|
||||
|
||||
describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
})
|
||||
|
||||
context('Runs Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
})
|
||||
|
||||
it('resolves the runs page', () => {
|
||||
cy.loginUser()
|
||||
cy.visitApp()
|
||||
@@ -33,6 +33,12 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
})
|
||||
|
||||
context('Runs - Login', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
})
|
||||
|
||||
it('when logged out, shows call to action', () => {
|
||||
cy.visitApp()
|
||||
cy.get('[href="#/runs"]').click()
|
||||
@@ -131,9 +137,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
|
||||
context('Runs - Connect Project', () => {
|
||||
it('opens Connect Project modal after clicking Connect Project button', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {}')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests', ['--config-file', 'cypressWithoutProjectId.config.js'])
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
cy.visitApp()
|
||||
@@ -167,9 +173,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
|
||||
context('Runs - Cannot Find Project', () => {
|
||||
beforeEach(() => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {\'projectId\': \'abcdef42\'}')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests', ['--config-file', 'cypressWithInvalidProjectId.config.js'])
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
@@ -217,9 +223,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
|
||||
context('Runs - Unauthorized Project', () => {
|
||||
beforeEach(() => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {\'projectId\': \'abcdef\'}')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
})
|
||||
@@ -278,9 +284,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
|
||||
context('Runs - Unauthorized Project Requested', () => {
|
||||
beforeEach(() => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {\'projectId\': \'abcdef\' }')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
@@ -310,9 +316,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
|
||||
context('Runs - No Runs', () => {
|
||||
it('when no runs and not connected, shows connect to dashboard button', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: null }')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests', ['--config-file', 'cypressWithoutProjectId.config.js'])
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
@@ -335,9 +341,9 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
})
|
||||
|
||||
it('displays how to record prompt when connected and no runs', () => {
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: \'abcdef\'}')
|
||||
})
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.loginUser()
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
@@ -360,8 +366,11 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
})
|
||||
|
||||
it('displays a copy button', () => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {projectId: \'abcdef\'}')
|
||||
o.sinon.stub(ctx.electronApi, 'copyTextToClipboard')
|
||||
})
|
||||
|
||||
@@ -389,6 +398,12 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
})
|
||||
|
||||
context('Runs - Runs List', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
})
|
||||
|
||||
it('displays a list of recorded runs if a run has been recorded', () => {
|
||||
cy.loginUser()
|
||||
cy.visitApp()
|
||||
@@ -446,6 +461,12 @@ describe('App: Runs', { viewportWidth: 1200 }, () => {
|
||||
})
|
||||
|
||||
describe('no internet connection', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('component-tests')
|
||||
cy.openProject('component-tests')
|
||||
cy.startAppServer('component')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
cy.goOnline()
|
||||
})
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('Sidebar Navigation', () => {
|
||||
})
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx.actions.project, 'setCurrentTestingType')
|
||||
o.sinon.stub(ctx.actions.project, 'setAndLoadCurrentTestingType')
|
||||
o.sinon.stub(ctx.actions.project, 'reconfigureProject').resolves()
|
||||
})
|
||||
|
||||
@@ -186,7 +186,7 @@ describe('Sidebar Navigation', () => {
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
expect(ctx.coreData.app.relaunchBrowser).eq(true)
|
||||
expect(ctx.actions.project.setCurrentTestingType).to.have.been.calledWith('component')
|
||||
expect(ctx.actions.project.setAndLoadCurrentTestingType).to.have.been.calledWith('component')
|
||||
expect(ctx.actions.project.reconfigureProject).to.have.been.called
|
||||
})
|
||||
|
||||
@@ -327,7 +327,7 @@ describe('Sidebar Navigation', () => {
|
||||
}).should('be.visible')
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
o.sinon.stub(ctx.actions.project, 'setCurrentTestingType')
|
||||
o.sinon.stub(ctx.actions.project, 'setAndLoadCurrentTestingType')
|
||||
o.sinon.stub(ctx.actions.project, 'reconfigureProject').resolves()
|
||||
})
|
||||
|
||||
@@ -337,7 +337,7 @@ describe('Sidebar Navigation', () => {
|
||||
|
||||
cy.withCtx((ctx) => {
|
||||
expect(ctx.coreData.app.relaunchBrowser).eq(true)
|
||||
expect(ctx.actions.project.setCurrentTestingType).to.have.been.calledWith('e2e')
|
||||
expect(ctx.actions.project.setAndLoadCurrentTestingType).to.have.been.calledWith('e2e')
|
||||
expect(ctx.actions.project.reconfigureProject).to.have.been.called
|
||||
})
|
||||
})
|
||||
|
||||
@@ -430,9 +430,13 @@ export class DataContext {
|
||||
assert(!this.coreData.hasInitializedMode)
|
||||
this.coreData.hasInitializedMode = this._config.mode
|
||||
if (this._config.mode === 'run') {
|
||||
await this.lifecycleManager.initializeRunMode()
|
||||
await this.lifecycleManager.initializeRunMode(this.coreData.currentTestingType)
|
||||
} else if (this._config.mode === 'open') {
|
||||
await this.initializeOpenMode()
|
||||
if (this.coreData.currentTestingType && await this.lifecycleManager.waitForInitializeSuccess()) {
|
||||
this.lifecycleManager.setAndLoadCurrentTestingType(this.coreData.currentTestingType)
|
||||
this.lifecycleManager.scaffoldFilesIfNecessary()
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Missing DataContext config "mode" setting, expected run | open`)
|
||||
}
|
||||
|
||||
@@ -290,9 +290,8 @@ export class MigrationActions {
|
||||
}
|
||||
|
||||
async finishReconfigurationWizard () {
|
||||
this.ctx.lifecycleManager.initializeConfigWatchers()
|
||||
this.ctx.lifecycleManager.refreshMetaState()
|
||||
await this.ctx.lifecycleManager.reloadConfig()
|
||||
await this.ctx.lifecycleManager.refreshLifecycle()
|
||||
}
|
||||
|
||||
async nextStep () {
|
||||
|
||||
@@ -95,8 +95,8 @@ export class ProjectActions {
|
||||
execa(this.ctx.coreData.localSettings.preferences.preferredEditorBinary, [projectPath])
|
||||
}
|
||||
|
||||
setCurrentTestingType (type: TestingType) {
|
||||
this.ctx.lifecycleManager.setCurrentTestingType(type)
|
||||
setAndLoadCurrentTestingType (type: TestingType) {
|
||||
this.ctx.lifecycleManager.setAndLoadCurrentTestingType(type)
|
||||
}
|
||||
|
||||
async setCurrentProject (projectRoot: string) {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import fs from 'fs-extra'
|
||||
import { getError } from '@packages/errors'
|
||||
|
||||
type CypressEnvOptions = {
|
||||
envFilePath: string
|
||||
validateConfigFile: (file: string | false, config: Cypress.ConfigOptions) => void
|
||||
}
|
||||
|
||||
export class CypressEnv {
|
||||
constructor (private options: CypressEnvOptions) {}
|
||||
|
||||
async loadCypressEnvFile (): Promise<Cypress.ConfigOptions> {
|
||||
return this.readAndValidateCypressEnvFile()
|
||||
}
|
||||
|
||||
private async readAndValidateCypressEnvFile () {
|
||||
const cypressEnv = await this.readCypressEnvFile()
|
||||
|
||||
this.options.validateConfigFile(this.options.envFilePath, cypressEnv)
|
||||
|
||||
return cypressEnv
|
||||
}
|
||||
|
||||
private async readCypressEnvFile (): Promise<Cypress.ConfigOptions> {
|
||||
try {
|
||||
return await fs.readJSON(this.options.envFilePath)
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (err.isCypressErr) {
|
||||
throw err
|
||||
}
|
||||
|
||||
throw getError('ERROR_READING_FILE', this.options.envFilePath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import debugLib from 'debug'
|
||||
import _ from 'lodash'
|
||||
|
||||
const debug = debugLib(`cypress:lifecycle:EventRegistrar`)
|
||||
|
||||
export class EventRegistrar {
|
||||
private _registeredEvents: Record<string, Function> = {}
|
||||
|
||||
hasNodeEvent (eventName: string) {
|
||||
const isRegistered = typeof this._registeredEvents[eventName] === 'function'
|
||||
|
||||
debug('plugin event registered? %o', { eventName, isRegistered })
|
||||
|
||||
return isRegistered
|
||||
}
|
||||
|
||||
executeNodeEvent (event: string, args: any[]) {
|
||||
debug(`execute plugin event '${event}' Node '${process.version}' with args: %o %o %o`, ...args)
|
||||
|
||||
const evtFn = this._registeredEvents[event]
|
||||
|
||||
if (typeof evtFn !== 'function') {
|
||||
throw new Error(`Missing event for ${event}`)
|
||||
}
|
||||
|
||||
return evtFn(...args)
|
||||
}
|
||||
|
||||
registerEvent (event: string, callback: Function) {
|
||||
debug(`register event '${event}'`)
|
||||
|
||||
if (!_.isString(event)) {
|
||||
throw new Error(`The plugin register function must be called with an event as its 1st argument. You passed '${event}'.`)
|
||||
}
|
||||
|
||||
if (!_.isFunction(callback)) {
|
||||
throw new Error(`The plugin register function must be called with a callback function as its 2nd argument. You passed '${callback}'.`)
|
||||
}
|
||||
|
||||
this._registeredEvents[event] = callback
|
||||
}
|
||||
|
||||
reset () {
|
||||
this._registeredEvents = {}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export class LegacyPluginsIpc extends EventEmitter {
|
||||
|
||||
send(event: 'loadLegacyPlugins', legacyConfig: LegacyCypressConfigJson): boolean
|
||||
send (event: string, ...args: any[]) {
|
||||
if (this.childProcess.killed) {
|
||||
if (this.childProcess.killed || !this.childProcess.connected) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
/* eslint-disable no-dupe-class-members */
|
||||
import type { CypressError } from '@packages/errors'
|
||||
import type { TestingType } from '@packages/types'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import { CypressError, getError } from '@packages/errors'
|
||||
import type { FullConfig, TestingType } from '@packages/types'
|
||||
import { ChildProcess, fork, ForkOptions } from 'child_process'
|
||||
import EventEmitter from 'events'
|
||||
import path from 'path'
|
||||
import inspector from 'inspector'
|
||||
import debugLib from 'debug'
|
||||
import { autoBindDebug } from '../util'
|
||||
import _ from 'lodash'
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
const debug = debugLib(`cypress:lifecycle:ProjectConfigIpc`)
|
||||
|
||||
const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child')
|
||||
|
||||
export type IpcHandler = (ipc: ProjectConfigIpc) => void
|
||||
|
||||
@@ -29,18 +38,27 @@ export interface SerializedLoadConfigReply {
|
||||
*
|
||||
*/
|
||||
export class ProjectConfigIpc extends EventEmitter {
|
||||
constructor (readonly childProcess: ChildProcess) {
|
||||
private _childProcess: ChildProcess
|
||||
|
||||
constructor (
|
||||
readonly nodePath: string | undefined | null,
|
||||
readonly projectRoot: string,
|
||||
readonly configFilePath: string,
|
||||
readonly configFile: string | false,
|
||||
readonly onError: (cypressError: CypressError, title?: string | undefined) => void,
|
||||
readonly onWarning: (cypressError: CypressError) => void,
|
||||
) {
|
||||
super()
|
||||
childProcess.on('error', (err) => {
|
||||
this._childProcess = this.forkConfigProcess()
|
||||
this._childProcess.on('error', (err) => {
|
||||
// this.emit('error', err)
|
||||
})
|
||||
|
||||
childProcess.on('message', (msg: { event: string, args: any[] }) => {
|
||||
this._childProcess.on('message', (msg: { event: string, args: any[] }) => {
|
||||
this.emit(msg.event, ...msg.args)
|
||||
})
|
||||
|
||||
childProcess.once('disconnect', () => {
|
||||
// console.log('Disconnected')
|
||||
this._childProcess.once('disconnect', () => {
|
||||
this.emit('disconnect')
|
||||
})
|
||||
|
||||
@@ -52,11 +70,11 @@ export class ProjectConfigIpc extends EventEmitter {
|
||||
send(event: 'setupTestingType', testingType: TestingType, options: Cypress.PluginConfigOptions): boolean
|
||||
send(event: 'loadConfig'): boolean
|
||||
send (event: string, ...args: any[]) {
|
||||
if (this.childProcess.killed) {
|
||||
if (this._childProcess.killed || !this._childProcess.connected) {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.childProcess.send({ event, args })
|
||||
return this._childProcess.send({ event, args })
|
||||
}
|
||||
|
||||
on(evt: 'childProcess:unhandledError', listener: (err: CypressError) => void): this
|
||||
@@ -88,4 +106,168 @@ export class ProjectConfigIpc extends EventEmitter {
|
||||
emit (evt: string, ...args: any[]) {
|
||||
return super.emit(evt, ...args)
|
||||
}
|
||||
|
||||
loadConfig (): Promise<LoadConfigReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this._childProcess.stdout && this._childProcess.stderr) {
|
||||
// manually pipe plugin stdout and stderr for dashboard capture
|
||||
// @see https://github.com/cypress-io/cypress/issues/7434
|
||||
this._childProcess.stdout.on('data', (data) => process.stdout.write(data))
|
||||
this._childProcess.stderr.on('data', (data) => process.stderr.write(data))
|
||||
}
|
||||
|
||||
let resolved = false
|
||||
|
||||
this._childProcess.on('error', (err) => {
|
||||
this.handleChildProcessError(err, this, resolved, reject)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
/**
|
||||
* This reject cannot be caught anywhere??
|
||||
*
|
||||
* It's supposed to be caught on lib/modes/run.js:1689,
|
||||
* but it's not.
|
||||
*/
|
||||
this.on('childProcess:unhandledError', (err) => {
|
||||
this.handleChildProcessError(err, this, resolved, reject)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
this.once('loadConfig:reply', (val) => {
|
||||
debug('loadConfig:reply')
|
||||
resolve({ ...val, initialConfig: JSON.parse(val.initialConfig) })
|
||||
resolved = true
|
||||
})
|
||||
|
||||
this.once('loadConfig:error', (err) => {
|
||||
this.killChildProcess()
|
||||
reject(err)
|
||||
})
|
||||
|
||||
debug('trigger the load of the file')
|
||||
this.once('ready', () => {
|
||||
this.send('loadConfig')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async callSetupNodeEventsWithConfig (testingType: TestingType, config: FullConfig, handlers: IpcHandler[]): Promise<SetupNodeEventsReply> {
|
||||
for (const handler of handlers) {
|
||||
handler(this)
|
||||
}
|
||||
|
||||
const promise = this.registerSetupIpcHandlers()
|
||||
|
||||
const overrides = config[testingType] ?? {}
|
||||
const mergedConfig = { ...config, ...overrides }
|
||||
|
||||
// alphabetize config by keys
|
||||
let orderedConfig = {} as Cypress.PluginConfigOptions
|
||||
|
||||
Object.keys(mergedConfig).sort().forEach((key) => {
|
||||
const k = key as keyof typeof mergedConfig
|
||||
|
||||
// @ts-ignore
|
||||
orderedConfig[k] = mergedConfig[k]
|
||||
})
|
||||
|
||||
this.send('setupTestingType', testingType, {
|
||||
...orderedConfig,
|
||||
projectRoot: this.projectRoot,
|
||||
configFile: this.configFilePath,
|
||||
version: pkg.version,
|
||||
testingType,
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
private registerSetupIpcHandlers (): Promise<SetupNodeEventsReply> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let resolved = false
|
||||
|
||||
this._childProcess.on('error', (err) => {
|
||||
this.handleChildProcessError(err, this, resolved, reject)
|
||||
reject(err)
|
||||
})
|
||||
|
||||
// For every registration event, we want to turn into an RPC with the child process
|
||||
this.once('setupTestingType:reply', (val) => {
|
||||
resolved = true
|
||||
resolve(val)
|
||||
})
|
||||
|
||||
this.once('setupTestingType:error', (err) => {
|
||||
reject(err)
|
||||
})
|
||||
|
||||
const handleWarning = (warningErr: CypressError) => {
|
||||
debug('plugins process warning:', warningErr.stack)
|
||||
|
||||
return this.onWarning(warningErr)
|
||||
}
|
||||
|
||||
this.on('warning', handleWarning)
|
||||
})
|
||||
}
|
||||
|
||||
private forkConfigProcess () {
|
||||
const configProcessArgs = ['--projectRoot', this.projectRoot, '--file', this.configFilePath]
|
||||
// allow the use of ts-node in subprocesses tests by removing the env constant from it
|
||||
// without this line, packages/ts/register.js never registers the ts-node module for config and
|
||||
// run_plugins can't use the config module.
|
||||
const { CYPRESS_INTERNAL_E2E_TESTING_SELF, ...env } = process.env
|
||||
|
||||
env.NODE_OPTIONS = process.env.ORIGINAL_NODE_OPTIONS || ''
|
||||
|
||||
const childOptions: ForkOptions = {
|
||||
stdio: 'pipe',
|
||||
cwd: path.dirname(this.configFilePath),
|
||||
env,
|
||||
execPath: this.nodePath ?? undefined,
|
||||
}
|
||||
|
||||
if (inspector.url()) {
|
||||
childOptions.execArgv = _.chain(process.execArgv.slice(0))
|
||||
.remove('--inspect-brk')
|
||||
.push(`--inspect=${process.debugPort + 1}`)
|
||||
.value()
|
||||
}
|
||||
|
||||
debug('fork child process', CHILD_PROCESS_FILE_PATH, configProcessArgs, _.omit(childOptions, 'env'))
|
||||
|
||||
const proc = fork(CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions)
|
||||
|
||||
return proc
|
||||
}
|
||||
|
||||
private handleChildProcessError (err: any, ipc: ProjectConfigIpc, resolved: boolean, reject: (reason?: any) => void) {
|
||||
debug('plugins process error:', err.stack)
|
||||
|
||||
this.cleanupIpc()
|
||||
|
||||
err = getError('CONFIG_FILE_UNEXPECTED_ERROR', this.configFile || '(unknown config file)', err)
|
||||
err.title = 'Config process error'
|
||||
|
||||
// this can sometimes trigger before the promise is fulfilled and
|
||||
// sometimes after, so we need to handle each case differently
|
||||
if (resolved) {
|
||||
this.onError(err)
|
||||
} else {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
cleanupIpc () {
|
||||
this.killChildProcess()
|
||||
this.removeAllListeners()
|
||||
}
|
||||
|
||||
private killChildProcess () {
|
||||
this._childProcess.kill()
|
||||
this._childProcess.stdout?.removeAllListeners()
|
||||
this._childProcess.stderr?.removeAllListeners()
|
||||
this._childProcess.removeAllListeners()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
import { CypressError, getError } from '@packages/errors'
|
||||
import { IpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc'
|
||||
import assert from 'assert'
|
||||
import type { AllModeOptions, FullConfig, TestingType } from '@packages/types'
|
||||
import debugLib from 'debug'
|
||||
import path from 'path'
|
||||
import _ from 'lodash'
|
||||
import chokidar from 'chokidar'
|
||||
import { validate as validateConfig, validateNoBreakingConfigLaunchpad, validateNoBreakingConfigRoot, validateNoBreakingTestingTypeConfig } from '@packages/config'
|
||||
import { CypressEnv } from './CypressEnv'
|
||||
import { autoBindDebug } from '../util/autoBindDebug'
|
||||
import type { EventRegistrar } from './EventRegistrar'
|
||||
import type { DataContext } from '../DataContext'
|
||||
|
||||
const debug = debugLib(`cypress:lifecycle:ProjectConfigManager`)
|
||||
|
||||
const UNDEFINED_SERIALIZED = '__cypress_undefined__'
|
||||
|
||||
type ProjectConfigManagerOptions = {
|
||||
ctx: DataContext
|
||||
configFile: string | false
|
||||
projectRoot: string
|
||||
handlers: IpcHandler[]
|
||||
hasCypressEnvFile: boolean
|
||||
eventRegistrar: EventRegistrar
|
||||
onError: (cypressError: CypressError, title?: string | undefined) => void
|
||||
onInitialConfigLoaded: (initialConfig: Cypress.ConfigOptions) => void
|
||||
onFinalConfigLoaded: (finalConfig: FullConfig) => Promise<void>
|
||||
refreshLifecycle: () => Promise<boolean>
|
||||
}
|
||||
|
||||
type ConfigManagerState = 'pending' | 'loadingConfig' | 'loadedConfig' | 'loadingNodeEvents' | 'ready' | 'errored'
|
||||
|
||||
export class ProjectConfigManager {
|
||||
private _configFilePath: string | undefined
|
||||
private _cachedFullConfig: FullConfig | undefined
|
||||
private _eventsIpc?: ProjectConfigIpc
|
||||
private _pathToWatcherRecord: Record<string, chokidar.FSWatcher> = {}
|
||||
private _watchers = new Set<chokidar.FSWatcher>()
|
||||
private _registeredEventsTarget: TestingType | undefined
|
||||
private _testingType: TestingType | null = null
|
||||
private _state: ConfigManagerState = 'pending'
|
||||
private _loadConfigPromise: Promise<LoadConfigReply> | undefined
|
||||
private _cachedLoadConfig: LoadConfigReply | undefined
|
||||
private _cypressEnv: CypressEnv
|
||||
|
||||
constructor (private options: ProjectConfigManagerOptions) {
|
||||
this._cypressEnv = new CypressEnv({
|
||||
envFilePath: this.envFilePath,
|
||||
validateConfigFile: (filePath, config) => {
|
||||
this.validateConfigFile(filePath, config)
|
||||
},
|
||||
})
|
||||
|
||||
return autoBindDebug(this)
|
||||
}
|
||||
|
||||
get isLoadingNodeEvents () {
|
||||
return this._state === 'loadingNodeEvents'
|
||||
}
|
||||
|
||||
get isReady () {
|
||||
return this._state === 'ready'
|
||||
}
|
||||
|
||||
get isLoadingConfigFile () {
|
||||
return this._state === 'loadingConfig'
|
||||
}
|
||||
|
||||
get isInError () {
|
||||
return this._state === 'errored'
|
||||
}
|
||||
|
||||
get configFilePath () {
|
||||
assert(this._configFilePath, 'configFilePath is undefined')
|
||||
|
||||
return this._configFilePath
|
||||
}
|
||||
|
||||
set configFilePath (configFilePath) {
|
||||
this._configFilePath = configFilePath
|
||||
}
|
||||
|
||||
setTestingType (testingType: TestingType | null) {
|
||||
this._testingType = testingType
|
||||
}
|
||||
|
||||
private get envFilePath () {
|
||||
return path.join(this.options.projectRoot, 'cypress.env.json')
|
||||
}
|
||||
|
||||
private get loadedConfigFile (): Partial<Cypress.ConfigOptions> | null {
|
||||
return this._cachedLoadConfig?.initialConfig ?? null
|
||||
}
|
||||
|
||||
async initializeConfig (): Promise<LoadConfigReply['initialConfig']> {
|
||||
try {
|
||||
this._state = 'loadingConfig'
|
||||
|
||||
// Clean things up for a new load
|
||||
this.closeWatchers()
|
||||
this._cachedLoadConfig = undefined
|
||||
this._cachedFullConfig = undefined
|
||||
|
||||
const loadConfigReply = await this.loadConfig()
|
||||
|
||||
// This is necessary as there is a weird timing issue where an error occurs and the config results get loaded
|
||||
// TODO: see if this can be !== 'errored'
|
||||
if (this._state === 'loadingConfig') {
|
||||
debug(`config is loaded for file`, this.configFilePath, this._testingType)
|
||||
this.validateConfigFile(this.configFilePath, loadConfigReply.initialConfig)
|
||||
|
||||
this._state = 'loadedConfig'
|
||||
this._cachedLoadConfig = loadConfigReply
|
||||
|
||||
this.options.onInitialConfigLoaded(loadConfigReply.initialConfig)
|
||||
|
||||
this.watchFiles([
|
||||
...loadConfigReply.requires,
|
||||
this.configFilePath,
|
||||
])
|
||||
}
|
||||
|
||||
return loadConfigReply.initialConfig
|
||||
} catch (error) {
|
||||
debug(`catch %o`, error)
|
||||
if (this._eventsIpc) {
|
||||
this._eventsIpc.cleanupIpc()
|
||||
}
|
||||
|
||||
this._state = 'errored'
|
||||
this.closeWatchers()
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
this.options.ctx.emitter.toLaunchpad()
|
||||
}
|
||||
}
|
||||
|
||||
loadTestingType () {
|
||||
// If we have set a testingType, and it's not the "target" of the
|
||||
// registeredEvents (switching testing mode), we need to get a fresh
|
||||
// config IPC & re-execute the setupTestingType
|
||||
if (this._registeredEventsTarget && this._testingType !== this._registeredEventsTarget) {
|
||||
this.options.refreshLifecycle().catch(this.onLoadError)
|
||||
} else if (this._eventsIpc && !this._registeredEventsTarget && this._cachedLoadConfig) {
|
||||
this.setupNodeEvents(this._cachedLoadConfig).catch(this.onLoadError)
|
||||
}
|
||||
}
|
||||
|
||||
private async setupNodeEvents (loadConfigReply: LoadConfigReply): Promise<void> {
|
||||
assert(this._eventsIpc, 'Expected _eventsIpc to be defined at this point')
|
||||
this._state = 'loadingNodeEvents'
|
||||
|
||||
try {
|
||||
assert(this._testingType, 'Cannot setup node events without a testing type')
|
||||
this._registeredEventsTarget = this._testingType
|
||||
const config = await this.getFullInitialConfig()
|
||||
const setupNodeEventsReply = await this._eventsIpc?.callSetupNodeEventsWithConfig(this._testingType, config, this.options.handlers)
|
||||
|
||||
await this.handleSetupTestingTypeReply(this._eventsIpc, loadConfigReply, setupNodeEventsReply)
|
||||
this._state = 'ready'
|
||||
} catch (error) {
|
||||
debug(`catch setupNodeEvents %o`, error)
|
||||
this._state = 'errored'
|
||||
if (this._eventsIpc) {
|
||||
this._eventsIpc.cleanupIpc()
|
||||
}
|
||||
|
||||
this.closeWatchers()
|
||||
|
||||
throw error
|
||||
} finally {
|
||||
this.options.ctx.emitter.toLaunchpad()
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSetupTestingTypeReply (ipc: ProjectConfigIpc, loadConfigReply: LoadConfigReply, result: SetupNodeEventsReply) {
|
||||
this.options.eventRegistrar.reset()
|
||||
|
||||
for (const { event, eventId } of result.registrations) {
|
||||
debug('register plugins process event', event, 'with id', eventId)
|
||||
|
||||
this.options.eventRegistrar.registerEvent(event, function (...args: any[]) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const invocationId = _.uniqueId('inv')
|
||||
|
||||
debug('call event', event, 'for invocation id', invocationId)
|
||||
|
||||
ipc.once(`promise:fulfilled:${invocationId}`, (err: any, value: any) => {
|
||||
if (err) {
|
||||
debug('promise rejected for id %s %o', invocationId, ':', err.stack)
|
||||
reject(_.extend(new Error(err.message), err))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (value === UNDEFINED_SERIALIZED) {
|
||||
value = undefined
|
||||
}
|
||||
|
||||
debug(`promise resolved for id '${invocationId}' with value`, value)
|
||||
|
||||
return resolve(value)
|
||||
})
|
||||
|
||||
const ids = { invocationId, eventId }
|
||||
|
||||
// no argument is passed for cy.task()
|
||||
// This is necessary because undefined becomes null when it is sent through ipc.
|
||||
if (event === 'task' && args[1] === undefined) {
|
||||
args[1] = {
|
||||
__cypress_task_no_argument__: true,
|
||||
}
|
||||
}
|
||||
|
||||
ipc.send('execute:plugins', event, ids, args)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const cypressEnv = await this.loadCypressEnvFile()
|
||||
const fullConfig = await this.buildBaseFullConfig(loadConfigReply.initialConfig, cypressEnv, this.options.ctx.modeOptions)
|
||||
|
||||
const finalConfig = this._cachedFullConfig = this.options.ctx._apis.configApi.updateWithPluginValues(fullConfig, result.setupConfig ?? {})
|
||||
|
||||
await this.options.onFinalConfigLoaded(finalConfig)
|
||||
|
||||
this.watchFiles([
|
||||
...result.requires,
|
||||
this.envFilePath,
|
||||
])
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
resetLoadingState () {
|
||||
this._loadConfigPromise = undefined
|
||||
this._registeredEventsTarget = undefined
|
||||
this._state = 'pending'
|
||||
}
|
||||
|
||||
private loadConfig () {
|
||||
if (!this._loadConfigPromise) {
|
||||
// If there's already a dangling IPC from the previous switch of testing type, we want to clean this up
|
||||
if (this._eventsIpc) {
|
||||
this._eventsIpc.cleanupIpc()
|
||||
}
|
||||
|
||||
this._eventsIpc = new ProjectConfigIpc(this.options.ctx.nodePath, this.options.projectRoot, this.configFilePath, this.options.configFile, (cypressError: CypressError, title?: string | undefined) => {
|
||||
this._state = 'errored'
|
||||
this.options.ctx.onError(cypressError, title)
|
||||
}, this.options.ctx.onWarning)
|
||||
|
||||
this._loadConfigPromise = this._eventsIpc.loadConfig()
|
||||
}
|
||||
|
||||
return this._loadConfigPromise
|
||||
}
|
||||
|
||||
private validateConfigFile (file: string | false, config: Cypress.ConfigOptions) {
|
||||
validateConfig(config, (errMsg) => {
|
||||
if (_.isString(errMsg)) {
|
||||
throw getError('CONFIG_VALIDATION_MSG_ERROR', 'configFile', file || null, errMsg)
|
||||
}
|
||||
|
||||
throw getError('CONFIG_VALIDATION_ERROR', 'configFile', file || null, errMsg)
|
||||
})
|
||||
|
||||
return validateNoBreakingConfigLaunchpad(
|
||||
config,
|
||||
(type, obj) => {
|
||||
const error = getError(type, obj)
|
||||
|
||||
this.options.ctx.onWarning(error)
|
||||
|
||||
return error
|
||||
},
|
||||
(type, obj) => {
|
||||
const error = getError(type, obj)
|
||||
|
||||
this.options.onError(error)
|
||||
|
||||
throw error
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
onLoadError = (error: any) => {
|
||||
this.closeWatchers()
|
||||
this.options.onError(error, 'Error Loading Config')
|
||||
}
|
||||
|
||||
private watchFiles (paths: string[]) {
|
||||
if (this.options.ctx.isRunMode) {
|
||||
return
|
||||
}
|
||||
|
||||
const filtered = paths.filter((p) => !p.includes('/node_modules/'))
|
||||
|
||||
for (const path of filtered) {
|
||||
if (!this._pathToWatcherRecord[path]) {
|
||||
this._pathToWatcherRecord[path] = this.addWatcherFor(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addWatcherFor (file: string) {
|
||||
const w = this.addWatcher(file)
|
||||
|
||||
w.on('all', (evt) => {
|
||||
debug(`changed ${file}: ${evt}`)
|
||||
this.options.refreshLifecycle().catch(this.onLoadError)
|
||||
})
|
||||
|
||||
w.on('error', (err) => {
|
||||
debug('error watching config files %O', err)
|
||||
this.options.ctx.onWarning(getError('UNEXPECTED_INTERNAL_ERROR', err))
|
||||
})
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
private addWatcher (file: string | string[]) {
|
||||
const w = chokidar.watch(file, {
|
||||
ignoreInitial: true,
|
||||
cwd: this.options.projectRoot,
|
||||
})
|
||||
|
||||
this._watchers.add(w)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
private validateConfigRoot (config: Cypress.ConfigOptions) {
|
||||
return validateNoBreakingConfigRoot(
|
||||
config,
|
||||
(type, obj) => {
|
||||
return getError(type, obj)
|
||||
},
|
||||
(type, obj) => {
|
||||
throw getError(type, obj)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private validateTestingTypeConfig (config: Cypress.ConfigOptions, testingType: TestingType) {
|
||||
return validateNoBreakingTestingTypeConfig(
|
||||
config,
|
||||
testingType,
|
||||
(type, ...args) => {
|
||||
return getError(type, ...args)
|
||||
},
|
||||
(type, ...args) => {
|
||||
throw getError(type, ...args)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private async buildBaseFullConfig (configFileContents: Cypress.ConfigOptions, envFile: Cypress.ConfigOptions, options: Partial<AllModeOptions>, withBrowsers = true) {
|
||||
assert(this._testingType, 'Cannot build base full config without a testing type')
|
||||
this.validateConfigRoot(configFileContents)
|
||||
|
||||
const testingTypeOverrides = configFileContents[this._testingType] ?? {}
|
||||
const optionsOverrides = options.config?.[this._testingType] ?? {}
|
||||
|
||||
this.validateTestingTypeConfig(testingTypeOverrides, this._testingType)
|
||||
this.validateTestingTypeConfig(optionsOverrides, this._testingType)
|
||||
|
||||
// TODO: pass in options.config overrides separately, so they are reflected in the UI
|
||||
configFileContents = { ...configFileContents, ...testingTypeOverrides, ...optionsOverrides }
|
||||
|
||||
// TODO: Convert this to be synchronous, it's just FS checks
|
||||
let fullConfig = await this.options.ctx._apis.configApi.setupFullConfigWithDefaults({
|
||||
cliConfig: options.config ?? {},
|
||||
projectName: path.basename(this.options.projectRoot),
|
||||
projectRoot: this.options.projectRoot,
|
||||
config: _.cloneDeep(configFileContents),
|
||||
envFile: _.cloneDeep(envFile),
|
||||
options: {
|
||||
...options,
|
||||
testingType: this._testingType,
|
||||
},
|
||||
})
|
||||
|
||||
if (withBrowsers) {
|
||||
const browsers = await this.options.ctx.browser.machineBrowsers()
|
||||
|
||||
if (!fullConfig.browsers || fullConfig.browsers.length === 0) {
|
||||
// @ts-ignore - we don't know if the browser is headed or headless at this point.
|
||||
// this is handled in open_project#launch.
|
||||
fullConfig.browsers = browsers
|
||||
fullConfig.resolved.browsers = { 'value': fullConfig.browsers, 'from': 'runtime' }
|
||||
}
|
||||
|
||||
fullConfig.browsers = fullConfig.browsers?.map((browser) => {
|
||||
if (browser.family === 'chromium' || fullConfig.chromeWebSecurity) {
|
||||
return browser
|
||||
}
|
||||
|
||||
return {
|
||||
...browser,
|
||||
warning: browser.warning || getError('CHROME_WEB_SECURITY_NOT_SUPPORTED', browser.name).message,
|
||||
}
|
||||
})
|
||||
|
||||
// If we have withBrowsers set to false, it means we're coming from the legacy config.get API
|
||||
// in tests, which shouldn't be validating the config
|
||||
this.validateConfigFile(this.options.configFile, fullConfig)
|
||||
}
|
||||
|
||||
return _.cloneDeep(fullConfig)
|
||||
}
|
||||
|
||||
async getFullInitialConfig (options: Partial<AllModeOptions> = this.options.ctx.modeOptions, withBrowsers = true): Promise<FullConfig> {
|
||||
if (this._cachedFullConfig) {
|
||||
return this._cachedFullConfig
|
||||
}
|
||||
|
||||
const [configFileContents, envFile] = await Promise.all([
|
||||
this.getConfigFileContents(),
|
||||
this.reloadCypressEnvFile(),
|
||||
])
|
||||
|
||||
this._cachedFullConfig = await this.buildBaseFullConfig(configFileContents, envFile, options, withBrowsers)
|
||||
|
||||
return this._cachedFullConfig
|
||||
}
|
||||
|
||||
async getConfigFileContents () {
|
||||
if (this._cachedLoadConfig?.initialConfig) {
|
||||
return this._cachedLoadConfig?.initialConfig
|
||||
}
|
||||
|
||||
return this.initializeConfig()
|
||||
}
|
||||
|
||||
async loadCypressEnvFile () {
|
||||
return this._cypressEnv.loadCypressEnvFile()
|
||||
}
|
||||
|
||||
async reloadCypressEnvFile () {
|
||||
this._cypressEnv = new CypressEnv({
|
||||
envFilePath: this.envFilePath,
|
||||
validateConfigFile: (filePath, config) => {
|
||||
this.validateConfigFile(filePath, config)
|
||||
},
|
||||
})
|
||||
|
||||
return this._cypressEnv.loadCypressEnvFile()
|
||||
}
|
||||
|
||||
isTestingTypeConfigured (testingType: TestingType): boolean {
|
||||
const config = this.loadedConfigFile
|
||||
|
||||
if (!config) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!_.has(config, testingType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (testingType === 'component') {
|
||||
return Boolean(config.component?.devServer)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private closeWatchers () {
|
||||
for (const watcher of this._watchers.values()) {
|
||||
// We don't care if there's an error while closing the watcher,
|
||||
// the watch listener on our end is already removed synchronously by chokidar
|
||||
watcher.close().catch((e) => {})
|
||||
}
|
||||
this._watchers = new Set()
|
||||
this._pathToWatcherRecord = {}
|
||||
}
|
||||
|
||||
destroy () {
|
||||
if (this._eventsIpc) {
|
||||
this._eventsIpc.cleanupIpc()
|
||||
}
|
||||
|
||||
this._state = 'pending'
|
||||
this._cachedLoadConfig = undefined
|
||||
this._cachedFullConfig = undefined
|
||||
this._registeredEventsTarget = undefined
|
||||
this.closeWatchers()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -112,7 +112,7 @@ export interface CoreDataShape {
|
||||
cliBrowser: string | null
|
||||
cliTestingType: string | null
|
||||
chosenBrowser: FoundBrowser | null
|
||||
machineBrowsers: Promise<FoundBrowser[]> | FoundBrowser[] | null
|
||||
machineBrowsers: Promise<FoundBrowser[]> | null
|
||||
servers: {
|
||||
appServer?: Maybe<Server>
|
||||
appServerPort?: Maybe<number>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable padding-line-between-statements */
|
||||
// created by autobarrel, do not modify directly
|
||||
|
||||
export * from './CypressEnv'
|
||||
export * from './EventRegistrar'
|
||||
export * from './LegacyPluginsIpc'
|
||||
export * from './ProjectConfigIpc'
|
||||
export * from './ProjectConfigManager'
|
||||
export * from './ProjectLifecycleManager'
|
||||
export * from './coreDataShape'
|
||||
|
||||
@@ -12,6 +12,8 @@ export type {
|
||||
GlobalPubSub,
|
||||
} from './globalPubSub'
|
||||
|
||||
export * from './util/pluginHandlers'
|
||||
|
||||
import { globalPubSub } from './globalPubSub'
|
||||
|
||||
export { globalPubSub }
|
||||
|
||||
@@ -32,15 +32,12 @@ export class BrowserDataSource {
|
||||
if (!this.ctx.coreData.machineBrowsers) {
|
||||
const p = this.ctx._apis.browserApi.getBrowsers()
|
||||
|
||||
this.ctx.coreData.machineBrowsers = p
|
||||
p.then((browsers) => {
|
||||
if (this.ctx.coreData.machineBrowsers === p) {
|
||||
if (browsers[0]) {
|
||||
this.ctx.coreData.chosenBrowser = browsers[0]
|
||||
}
|
||||
|
||||
this.ctx.coreData.machineBrowsers = browsers
|
||||
this.ctx.coreData.machineBrowsers = p.then((browsers) => {
|
||||
if (browsers[0]) {
|
||||
this.ctx.coreData.chosenBrowser = browsers[0]
|
||||
}
|
||||
|
||||
return browsers
|
||||
}).catch((e) => {
|
||||
this.ctx.update((coreData) => {
|
||||
coreData.machineBrowsers = null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os from 'os'
|
||||
import chokidar from 'chokidar'
|
||||
import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec } from '@packages/types'
|
||||
import { FrontendFramework, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
|
||||
import { scanFSForAvailableDependency } from 'create-cypress-tests'
|
||||
@@ -258,7 +259,11 @@ export class ProjectDataSource {
|
||||
this.ctx.actions.project.setSpecs(specs)
|
||||
})
|
||||
|
||||
this._specWatcher = this.ctx.lifecycleManager.addWatcher(specPattern)
|
||||
this._specWatcher = chokidar.watch(specPattern, {
|
||||
ignoreInitial: true,
|
||||
cwd: projectRoot,
|
||||
})
|
||||
|
||||
this._specWatcher.on('add', onSpecsChanged)
|
||||
this._specWatcher.on('change', onSpecsChanged)
|
||||
this._specWatcher.on('unlink', onSpecsChanged)
|
||||
@@ -319,12 +324,17 @@ export class ProjectDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.stopSpecWatcher()
|
||||
}
|
||||
|
||||
stopSpecWatcher () {
|
||||
if (!this._specWatcher) {
|
||||
return
|
||||
}
|
||||
|
||||
this.ctx.lifecycleManager.closeWatcher(this._specWatcher)
|
||||
this._specWatcher.close().catch(() => {})
|
||||
this._specWatcher = null
|
||||
}
|
||||
|
||||
getCurrentSpecByAbsolute (absolute: string) {
|
||||
|
||||
@@ -2,6 +2,15 @@ import debugLib from 'debug'
|
||||
|
||||
const debugLibCache: Record<string, debugLib.Debugger> = {}
|
||||
|
||||
/**
|
||||
* This function enables trace logging on all function calls and setters for a given class.
|
||||
* If you add:
|
||||
*
|
||||
* return autoBindDebug(this)
|
||||
*
|
||||
* to the constructor of the class for which you want to enable logging, you can then
|
||||
* set DEBUG=cypress-trace:<ClassName> to utilize the logging
|
||||
*/
|
||||
export function autoBindDebug <T extends object> (obj: T): T {
|
||||
const ns = `cypress-trace:${obj.constructor.name}`
|
||||
const debug = debugLibCache[ns] = debugLibCache[ns] || debugLib(ns)
|
||||
|
||||
@@ -5,4 +5,5 @@ export * from './autoBindDebug'
|
||||
export * from './cached'
|
||||
export * from './config-file-updater'
|
||||
export * from './file'
|
||||
export * from './pluginHandlers'
|
||||
export * from './urqlCacheKeys'
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { IpcHandler } from '../data'
|
||||
|
||||
let pluginHandlers: IpcHandler[] = []
|
||||
|
||||
export const getServerPluginHandlers = () => {
|
||||
return pluginHandlers
|
||||
}
|
||||
|
||||
export const registerServerPluginHandler = (handler: IpcHandler) => {
|
||||
pluginHandlers.push(handler)
|
||||
}
|
||||
|
||||
export const resetPluginHandlers = () => {
|
||||
pluginHandlers = []
|
||||
}
|
||||
@@ -32,9 +32,7 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run')
|
||||
appApi: {} as AppApiShape,
|
||||
localSettingsApi: {} as LocalSettingsApiShape,
|
||||
authApi: {} as AuthApiShape,
|
||||
configApi: {
|
||||
getServerPluginHandlers: () => [],
|
||||
} as InjectedConfigApi,
|
||||
configApi: {} as InjectedConfigApi,
|
||||
projectApi: {} as ProjectApiShape,
|
||||
electronApi: {
|
||||
copyTextToClipboard: (text) => {},
|
||||
|
||||
@@ -144,9 +144,7 @@ describe('findSpecs', () => {
|
||||
appApi: {} as AppApiShape,
|
||||
localSettingsApi: {} as LocalSettingsApiShape,
|
||||
authApi: {} as AuthApiShape,
|
||||
configApi: {
|
||||
getServerPluginHandlers: () => [],
|
||||
} as InjectedConfigApi,
|
||||
configApi: {} as InjectedConfigApi,
|
||||
projectApi: {} as ProjectApiShape,
|
||||
electronApi: {} as ElectronApiShape,
|
||||
browserApi: {} as BrowserApiShape,
|
||||
|
||||
@@ -225,7 +225,8 @@ function startAppServer (mode: 'component' | 'e2e' = 'e2e') {
|
||||
return logInternal('startAppServer', (log) => {
|
||||
return cy.window({ log: false }).then((win) => {
|
||||
return cy.withCtx(async (ctx, o) => {
|
||||
ctx.actions.project.setCurrentTestingType(o.mode)
|
||||
await ctx.lifecycleManager.waitForInitializeSuccess()
|
||||
ctx.actions.project.setAndLoadCurrentTestingType(o.mode)
|
||||
const isInitialized = o.pDefer()
|
||||
const initializeActive = ctx.actions.project.initializeActiveProject
|
||||
const onErrorStub = o.sinon.stub(ctx, 'onError')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="props.gql.currentProject"
|
||||
class="flex justify-center m-24px"
|
||||
class="flex m-24px justify-center"
|
||||
>
|
||||
<Card
|
||||
v-for="tt in TESTING_TYPES"
|
||||
v-for="tt in testingTypes"
|
||||
:key="tt.key"
|
||||
:data-cy-testingType="tt.key"
|
||||
:title="tt.name"
|
||||
@@ -68,24 +68,26 @@ const emits = defineEmits<{
|
||||
(eventName: 'pick', testingType: TestingTypeEnum, currentTestingType: TestingTypeEnum): void
|
||||
}>()
|
||||
|
||||
const TESTING_TYPES = [
|
||||
{
|
||||
key: 'e2e',
|
||||
name: t('testingType.e2e.name'),
|
||||
description: t('testingType.e2e.description'),
|
||||
icon: IconE2E,
|
||||
iconSolid: IconE2ESolid,
|
||||
configured: props.gql.currentProject?.isE2EConfigured,
|
||||
},
|
||||
{
|
||||
key: 'component',
|
||||
name: t('testingType.component.name'),
|
||||
description: t('testingType.component.description'),
|
||||
icon: IconComponent,
|
||||
iconSolid: IconComponentSolid,
|
||||
configured: props.gql.currentProject?.isCTConfigured,
|
||||
},
|
||||
] as const
|
||||
const testingTypes = computed(() => {
|
||||
return [
|
||||
{
|
||||
key: 'e2e',
|
||||
name: t('testingType.e2e.name'),
|
||||
description: t('testingType.e2e.description'),
|
||||
icon: IconE2E,
|
||||
iconSolid: IconE2ESolid,
|
||||
configured: props.gql.currentProject?.isE2EConfigured,
|
||||
},
|
||||
{
|
||||
key: 'component',
|
||||
name: t('testingType.component.name'),
|
||||
description: t('testingType.component.description'),
|
||||
icon: IconComponent,
|
||||
iconSolid: IconComponentSolid,
|
||||
configured: props.gql.currentProject?.isCTConfigured,
|
||||
},
|
||||
] as const
|
||||
})
|
||||
|
||||
const currentTestingType = computed(() => props.gql.currentProject?.currentTestingType as TestingTypeEnum)
|
||||
|
||||
|
||||
@@ -1012,9 +1012,6 @@ type Mutation {
|
||||
"""While migrating to 10+ skip manual rename step"""
|
||||
migrateSkipManualRename: Query
|
||||
|
||||
"""Initialize the migration wizard to the first step"""
|
||||
migrateStart: Query
|
||||
|
||||
"""Open a path in preferred IDE"""
|
||||
openDirectoryInIDE(path: String!): Boolean
|
||||
openExternal(includeGraphqlPort: Boolean, url: String!): Boolean
|
||||
@@ -1031,15 +1028,15 @@ type Mutation {
|
||||
"""show the launchpad windows"""
|
||||
reconfigureProject: Boolean!
|
||||
|
||||
"""Re-initializes Cypress from the initial CLI options"""
|
||||
reinitializeCypress: Query
|
||||
|
||||
"""Remove project from projects array and cache"""
|
||||
removeProject(path: String!): Query
|
||||
|
||||
"""Reset the Auth State"""
|
||||
resetAuthState: Query!
|
||||
|
||||
"""Resets errors and attempts to reload the config"""
|
||||
resetErrorsAndLoadConfig: Query
|
||||
|
||||
"""
|
||||
Resets the latest version call to capture additional telemetry for the current user
|
||||
"""
|
||||
@@ -1049,10 +1046,10 @@ type Mutation {
|
||||
resetWizard: Boolean!
|
||||
scaffoldIntegration: [ScaffoldedFile!]!
|
||||
scaffoldTestingType: Query
|
||||
setAndLoadCurrentTestingType(testingType: TestingTypeEnum!): Query
|
||||
|
||||
"""Set active project to run tests on"""
|
||||
setCurrentProject(path: String!): Query
|
||||
setCurrentTestingType(testingType: TestingTypeEnum!): Query
|
||||
|
||||
"""
|
||||
Update local preferences (also known as appData). The payload, `value`, should be a `JSON.stringified()` object of the new values you'd like to persist. Example: `setPreferences (value: JSON.stringify({ lastOpened: Date.now() }))`
|
||||
|
||||
@@ -24,12 +24,17 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('reinitializeCypress', {
|
||||
t.field('resetErrorsAndLoadConfig', {
|
||||
type: Query,
|
||||
description: 'Re-initializes Cypress from the initial CLI options',
|
||||
description: 'Resets errors and attempts to reload the config',
|
||||
resolve: async (_, args, ctx) => {
|
||||
await ctx.reinitializeCypress(ctx.modeOptions)
|
||||
await ctx.initializeMode()
|
||||
ctx.update((d) => {
|
||||
d.baseError = null
|
||||
d.warnings = []
|
||||
})
|
||||
|
||||
// Wait for the project config to be reloaded
|
||||
await ctx.lifecycleManager.refreshLifecycle()
|
||||
|
||||
return {}
|
||||
},
|
||||
@@ -150,24 +155,28 @@ export const mutation = mutationType({
|
||||
t.field('clearCurrentTestingType', {
|
||||
type: 'Query',
|
||||
resolve: async (_, args, ctx) => {
|
||||
ctx.lifecycleManager.setCurrentTestingType(null)
|
||||
ctx.lifecycleManager.setAndLoadCurrentTestingType(null)
|
||||
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
t.field('setCurrentTestingType', {
|
||||
t.field('setAndLoadCurrentTestingType', {
|
||||
type: 'Query',
|
||||
args: {
|
||||
testingType: nonNull(arg({ type: TestingTypeEnum })),
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
ctx.actions.project.setCurrentTestingType(args.testingType)
|
||||
ctx.actions.project.setAndLoadCurrentTestingType(args.testingType)
|
||||
|
||||
// if necessary init the wizard for configuration
|
||||
if (ctx.coreData.currentTestingType
|
||||
&& !ctx.lifecycleManager.isTestingTypeConfigured(ctx.coreData.currentTestingType)) {
|
||||
await ctx.actions.wizard.initialize()
|
||||
|
||||
if (ctx.wizardData.chosenLanguage === 'ts') {
|
||||
ctx.lifecycleManager.scaffoldFilesIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
@@ -472,16 +481,6 @@ export const mutation = mutationType({
|
||||
},
|
||||
})
|
||||
|
||||
t.field('migrateStart', {
|
||||
description: 'Initialize the migration wizard to the first step',
|
||||
type: Query,
|
||||
resolve: async (_, args, ctx) => {
|
||||
await ctx.lifecycleManager._pendingMigrationInitialize?.promise
|
||||
|
||||
return {}
|
||||
},
|
||||
})
|
||||
|
||||
t.field('migrateRenameSpecs', {
|
||||
description: 'While migrating to 10+ renames files to match the new .cy pattern',
|
||||
type: Query,
|
||||
@@ -593,7 +592,7 @@ export const mutation = mutationType({
|
||||
}
|
||||
|
||||
// Wait for the project config to be reloaded
|
||||
await ctx.lifecycleManager.reloadConfig()
|
||||
await ctx.lifecycleManager.refreshLifecycle()
|
||||
|
||||
return {}
|
||||
},
|
||||
@@ -617,7 +616,7 @@ export const mutation = mutationType({
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
ctx.project.setRelaunchBrowser(true)
|
||||
ctx.actions.project.setCurrentTestingType(args.testingType)
|
||||
ctx.actions.project.setAndLoadCurrentTestingType(args.testingType)
|
||||
await ctx.actions.project.reconfigureProject()
|
||||
|
||||
return true
|
||||
@@ -633,7 +632,7 @@ export const mutation = mutationType({
|
||||
},
|
||||
resolve: async (source, args, ctx) => {
|
||||
ctx.actions.project.setForceReconfigureProjectByTestingType({ forceReconfigureProject: true, testingType: args.testingType })
|
||||
ctx.actions.project.setCurrentTestingType(args.testingType)
|
||||
ctx.actions.project.setAndLoadCurrentTestingType(args.testingType)
|
||||
|
||||
if (args.isApp) {
|
||||
await ctx.actions.project.reconfigureProject()
|
||||
|
||||
@@ -35,7 +35,7 @@ export const Query = objectType({
|
||||
t.field('migration', {
|
||||
type: Migration,
|
||||
description: 'Metadata about the migration, null if we aren\'t showing it',
|
||||
resolve: (root, args, ctx) => ctx.coreData.migration,
|
||||
resolve: (root, args, ctx) => ctx.coreData.migration.legacyConfigForMigration ? ctx.coreData.migration : null,
|
||||
})
|
||||
|
||||
t.nonNull.field('dev', {
|
||||
|
||||
@@ -281,6 +281,33 @@ describe('Choose a Browser Page', () => {
|
||||
cy.contains('button', 'Start E2E Testing in Firefox').should('be.visible')
|
||||
cy.findByRole('radio', { name: 'Firefox v5', checked: true }).should('be.visible')
|
||||
})
|
||||
|
||||
it('should return to welcome screen if user modifies the config file to not include the current testing type and recover', () => {
|
||||
cy.openProject('launchpad', ['--e2e'])
|
||||
cy.visitLaunchpad()
|
||||
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {}')
|
||||
})
|
||||
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress!')
|
||||
cy.contains('[data-cy-testingtype="e2e"]', 'Not Configured')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js',
|
||||
`module.exports = {
|
||||
e2e: {
|
||||
setupNodeEvents: (on, config) => config,
|
||||
supportFile: false,
|
||||
},
|
||||
}`)
|
||||
})
|
||||
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress!')
|
||||
cy.get('[data-cy-testingtype="e2e"]').should('not.contain', 'Not Configured')
|
||||
})
|
||||
})
|
||||
|
||||
describe('No System Browsers Detected', () => {
|
||||
|
||||
@@ -27,6 +27,8 @@ describe('Config files error handling', () => {
|
||||
await ctx.actions.file.removeFileInProject('cypress.config.js')
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress')
|
||||
})
|
||||
|
||||
@@ -61,6 +63,8 @@ describe('Config files error handling', () => {
|
||||
await ctx.actions.file.removeFileInProject('cypress.json')
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('h1').should('contain', 'Welcome to Cypress')
|
||||
})
|
||||
|
||||
@@ -80,6 +84,8 @@ describe('Config files error handling', () => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = { e2e: { supportFile: false } }')
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ describe('Error handling', () => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { e2e: { baseUrl: 'https://cypress.com', supportFile: false } }`)
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('body')
|
||||
.should('not.contain.text', 'Error Loading Config')
|
||||
})
|
||||
@@ -54,6 +56,8 @@ describe('Error handling', () => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {}')
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.get('body')
|
||||
.should('not.contain.text', 'Error Loading Config')
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { e2eProjectDirs } from '@packages/frontend-shared/cypress/e2e/support/e2eProjectDirs'
|
||||
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
|
||||
|
||||
const renameAutoStep = `[data-cy="migration-step renameAuto"]`
|
||||
const renameManualStep = `[data-cy="migration-step renameManual"]`
|
||||
@@ -6,6 +7,16 @@ const renameSupportStep = `[data-cy="migration-step renameSupport"]`
|
||||
const configFileStep = `[data-cy="migration-step configFile"]`
|
||||
const setupComponentStep = `[data-cy="migration-step setupComponent"]`
|
||||
|
||||
function getPathForPlatform (posixPath: string) {
|
||||
// @ts-ignore
|
||||
const cy = window.Cypress
|
||||
const platform = cy?.platform || JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config)).platform
|
||||
|
||||
if (platform === 'win32') return posixPath.replaceAll('/', '\\')
|
||||
|
||||
return posixPath
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
@@ -927,7 +938,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo
|
||||
})
|
||||
})
|
||||
|
||||
it('completes journey for migration-e2e-legacy-plugins-throws-error', () => {
|
||||
it('completes journey for migration-e2e-legacy-plugins-throws-error and recovers', () => {
|
||||
scaffoldAndVisitLaunchpad('migration-e2e-legacy-plugins-throws-error')
|
||||
// no steps are shown - we show the error that surfaced when executing pluginsFile.
|
||||
cy.get(renameAutoStep).should('not.exist')
|
||||
@@ -941,6 +952,14 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo
|
||||
cy.get('[data-testid="error-code-frame"]').contains(`cypress/plugins/index.js:2:9`)
|
||||
// correct error from pluginsFile
|
||||
cy.contains(`throw Error('Uh oh, there was an error!')`)
|
||||
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject(o.path, 'module.exports = (on, config) => {}')
|
||||
}, { path: getPathForPlatform('cypress/plugins/index.js') })
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
|
||||
cy.waitForWizard()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<BaseError
|
||||
v-if="query.data.value.baseError"
|
||||
:gql="query.data.value.baseError"
|
||||
:retry="reinitializeCypress"
|
||||
:retry="resetErrorsAndLoadConfig"
|
||||
/>
|
||||
<GlobalPage
|
||||
v-else-if="query.data.value.isInGlobalMode || !query.data.value?.currentProject"
|
||||
@@ -74,7 +74,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useMutation, useQuery } from '@urql/vue'
|
||||
import { MainLaunchpadQueryDocument, Main_ReinitializeCypressDocument } from './generated/graphql'
|
||||
import { MainLaunchpadQueryDocument, Main_ResetErrorsAndLoadConfigDocument } from './generated/graphql'
|
||||
import TestingTypeCards from './setup/TestingTypeCards.vue'
|
||||
import Wizard from './setup/Wizard.vue'
|
||||
import ScaffoldLanguageSelect from './setup/ScaffoldLanguageSelect.vue'
|
||||
@@ -127,16 +127,16 @@ query MainLaunchpadQuery {
|
||||
`
|
||||
|
||||
gql`
|
||||
mutation Main_ReinitializeCypress {
|
||||
reinitializeCypress {
|
||||
mutation Main_ResetErrorsAndLoadConfig {
|
||||
resetErrorsAndLoadConfig {
|
||||
...MainLaunchpadQueryData
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const mutation = useMutation(Main_ReinitializeCypressDocument)
|
||||
const mutation = useMutation(Main_ResetErrorsAndLoadConfigDocument)
|
||||
|
||||
const reinitializeCypress = () => {
|
||||
const resetErrorsAndLoadConfig = () => {
|
||||
if (!mutation.fetching.value) {
|
||||
mutation.executeMutation({})
|
||||
}
|
||||
|
||||
@@ -161,7 +161,6 @@ import {
|
||||
MigrationWizard_RenameSpecsDocument,
|
||||
MigrationWizard_RenameSupportDocument,
|
||||
MigrationWizard_SkipManualRenameDocument,
|
||||
MigrationWizard_StartDocument,
|
||||
MigrationWizard_RenameSpecsFolderDocument,
|
||||
} from '../generated/graphql'
|
||||
import { useI18n } from '@cy/i18n'
|
||||
@@ -204,25 +203,7 @@ const query = useQuery({ query: MigrationWizardQueryDocument, requestPolicy: 'ca
|
||||
const migration = computed(() => query.data.value?.migration)
|
||||
const steps = computed(() => migration.value?.filteredSteps || [])
|
||||
|
||||
// start migration
|
||||
|
||||
gql`
|
||||
mutation MigrationWizard_Start {
|
||||
migrateStart {
|
||||
migration {
|
||||
filteredSteps {
|
||||
id
|
||||
...MigrationStep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const start = useMutation(MigrationWizard_StartDocument)
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await start.executeMutation({ })
|
||||
await query.executeQuery()
|
||||
})
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ fragment TestingTypeCards on Query {
|
||||
|
||||
gql`
|
||||
mutation TestingTypeSelection($testingType: TestingTypeEnum!) {
|
||||
setCurrentTestingType(testingType: $testingType) {
|
||||
setAndLoadCurrentTestingType(testingType: $testingType) {
|
||||
currentProject {
|
||||
id
|
||||
currentTestingType
|
||||
|
||||
@@ -3,7 +3,7 @@ import Debug from 'debug'
|
||||
import _ from 'lodash'
|
||||
import path from 'path'
|
||||
import deepDiff from 'return-deep-diff'
|
||||
import type { ResolvedFromConfig, ResolvedConfigurationOptionSource, AllModeOptions, FullConfig } from '@packages/types'
|
||||
import type { ResolvedFromConfig, ResolvedConfigurationOptionSource } from '@packages/types'
|
||||
import * as configUtils from '@packages/config'
|
||||
import * as errors from './errors'
|
||||
import { getProcessEnvVars, CYPRESS_SPECIAL_ENV_VARS } from './util/config'
|
||||
@@ -78,20 +78,6 @@ export function isValidCypressInternalEnvValue (value) {
|
||||
return _.includes(names, value)
|
||||
}
|
||||
|
||||
export async function get (
|
||||
projectRoot,
|
||||
// Options are only used in testing
|
||||
options?: Partial<AllModeOptions>,
|
||||
): Promise<FullConfig> {
|
||||
const ctx = getCtx()
|
||||
|
||||
options ??= ctx.modeOptions
|
||||
|
||||
ctx.lifecycleManager.setCurrentProject(projectRoot)
|
||||
|
||||
return ctx.lifecycleManager.getFullInitialConfig(options, false)
|
||||
}
|
||||
|
||||
export function setupFullConfigWithDefaults (obj: Record<string, any> = {}) {
|
||||
debug('setting config object %o', obj)
|
||||
let { projectRoot, projectName, config, envFile, options, cliConfig } = obj
|
||||
|
||||
@@ -26,7 +26,6 @@ import { openExternal } from '@packages/server/lib/gui/links'
|
||||
import { getUserEditor } from './util/editors'
|
||||
import * as savedState from './saved_state'
|
||||
import appData from './util/app_data'
|
||||
import plugins from './plugins'
|
||||
import browsers from './browsers'
|
||||
import devServer from './plugins/dev-server'
|
||||
|
||||
@@ -56,15 +55,11 @@ export function makeDataContext (options: MakeDataContextOptions): DataContext {
|
||||
},
|
||||
},
|
||||
configApi: {
|
||||
getServerPluginHandlers: plugins.getServerPluginHandlers,
|
||||
allowedConfig: configUtils.allowed,
|
||||
cypressVersion: pkg.version,
|
||||
validateConfig: configUtils.validate,
|
||||
updateWithPluginValues: config.updateWithPluginValues,
|
||||
setupFullConfigWithDefaults: config.setupFullConfigWithDefaults,
|
||||
validateRootConfigBreakingChanges: configUtils.validateNoBreakingConfigRoot,
|
||||
validateLaunchpadConfigBreakingChanges: configUtils.validateNoBreakingConfigLaunchpad,
|
||||
validateTestingTypeConfigBreakingChanges: configUtils.validateNoBreakingTestingTypeConfig,
|
||||
},
|
||||
appApi: {
|
||||
appData,
|
||||
|
||||
@@ -253,7 +253,7 @@ export class OpenProject {
|
||||
|
||||
const testingType = args.testingType === 'component' ? 'component' : 'e2e'
|
||||
|
||||
this._ctx.lifecycleManager.setRunModeExitEarly(options.onError ?? undefined)
|
||||
this._ctx.lifecycleManager.runModeExitEarly = options.onError ?? undefined
|
||||
|
||||
// store the currently open project
|
||||
this.projectBase = new ProjectBase({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { getCtx } = require('@packages/data-context')
|
||||
const { getCtx, registerServerPluginHandler, getServerPluginHandlers: getPluginHandlers } = require('@packages/data-context')
|
||||
|
||||
const registerEvent = (event, callback) => {
|
||||
getCtx().lifecycleManager.registerEvent(event, callback)
|
||||
@@ -8,18 +8,12 @@ const getPluginPid = () => {
|
||||
return getCtx().lifecycleManager.eventProcessPid
|
||||
}
|
||||
|
||||
let handlers = []
|
||||
|
||||
const registerHandler = (handler) => {
|
||||
handlers.push(handler)
|
||||
registerServerPluginHandler(handler)
|
||||
}
|
||||
|
||||
const getServerPluginHandlers = () => {
|
||||
return handlers
|
||||
}
|
||||
|
||||
const init = (config, options) => {
|
||||
// return getCtx().lifecycleManager.ready()
|
||||
return getPluginHandlers()
|
||||
}
|
||||
|
||||
const has = (event) => {
|
||||
@@ -31,8 +25,6 @@ const execute = (event, ...args) => {
|
||||
}
|
||||
|
||||
const _reset = () => {
|
||||
handlers = []
|
||||
|
||||
return getCtx().lifecycleManager.reinitializeCypress()
|
||||
}
|
||||
|
||||
@@ -40,7 +32,6 @@ module.exports = {
|
||||
getPluginPid,
|
||||
execute,
|
||||
has,
|
||||
init,
|
||||
registerEvent,
|
||||
registerHandler,
|
||||
getServerPluginHandlers,
|
||||
|
||||
@@ -497,7 +497,7 @@ export class ProjectBase<TServer extends Server> extends EE {
|
||||
}
|
||||
|
||||
async initializeConfig (): Promise<Cfg> {
|
||||
this.ctx.lifecycleManager.setCurrentTestingType(this.testingType)
|
||||
this.ctx.lifecycleManager.setAndLoadCurrentTestingType(this.testingType)
|
||||
let theCfg: Cfg = {
|
||||
...(await this.ctx.lifecycleManager.getFullInitialConfig()),
|
||||
testingType: this.testingType,
|
||||
|
||||
@@ -27,10 +27,8 @@ const runMode = require(`../../lib/modes/run`)
|
||||
const api = require(`../../lib/api`)
|
||||
const cwd = require(`../../lib/cwd`)
|
||||
const user = require(`../../lib/user`)
|
||||
const config = require(`../../lib/config`)
|
||||
const cache = require(`../../lib/cache`)
|
||||
const errors = require(`../../lib/errors`)
|
||||
const plugins = require(`../../lib/plugins`)
|
||||
const cypress = require(`../../lib/cypress`)
|
||||
const ProjectBase = require(`../../lib/project-base`).ProjectBase
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
@@ -127,7 +125,6 @@ describe('lib/cypress', () => {
|
||||
// force cypress to call directly into main without
|
||||
// spawning a separate process
|
||||
sinon.stub(videoCapture, 'start').resolves({})
|
||||
sinon.stub(plugins, 'init').resolves(undefined)
|
||||
sinon.stub(electronApp, 'isRunning').returns(true)
|
||||
sinon.stub(extension, 'setHostAndPath').resolves()
|
||||
sinon.stub(detect, 'detect').resolves(TYPICAL_BROWSERS)
|
||||
@@ -457,7 +454,7 @@ describe('lib/cypress', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.pristineWithConfigPath)
|
||||
|
||||
return config.get(this.pristineWithConfigPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then(() => {
|
||||
return fs.rmdir(supportFolder, { recursive: true })
|
||||
}).then(() => {
|
||||
@@ -480,7 +477,7 @@ describe('lib/cypress', () => {
|
||||
it.skip('removes fixtures when they exist and fixturesFolder is false', function (done) {
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.idsPath)
|
||||
|
||||
config.get(this.idsPath)
|
||||
ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((cfg) => {
|
||||
this.cfg = cfg
|
||||
|
||||
@@ -540,7 +537,7 @@ describe('lib/cypress', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.idsPath)
|
||||
|
||||
return config.get(this.idsPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((cfg) => {
|
||||
this.cfg = cfg
|
||||
|
||||
@@ -897,8 +894,6 @@ describe('lib/cypress', () => {
|
||||
})
|
||||
|
||||
it('can override values in plugins', function () {
|
||||
plugins.init.restore()
|
||||
|
||||
return cypress.start([
|
||||
`--run-project=${this.pluginConfig}`, '--config=requestTimeout=1234,videoCompression=false',
|
||||
'--env=foo=foo,bar=bar',
|
||||
@@ -940,7 +935,6 @@ describe('lib/cypress', () => {
|
||||
|
||||
describe('plugins', () => {
|
||||
beforeEach(() => {
|
||||
plugins.init.restore()
|
||||
browsers.open.restore()
|
||||
|
||||
const ee = new EE()
|
||||
|
||||
@@ -19,7 +19,6 @@ const SseStream = require('ssestream')
|
||||
const EventSource = require('eventsource')
|
||||
const config = require(`../../lib/config`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const ProjectBase = require(`../../lib/project-base`).ProjectBase
|
||||
const pluginsModule = require(`../../lib/plugins`)
|
||||
const preprocessor = require(`../../lib/plugins/preprocessor`)
|
||||
const resolve = require(`../../lib/util/resolve`)
|
||||
@@ -103,7 +102,7 @@ describe('Routes', () => {
|
||||
obj.projectRoot = Fixtures.projectPath('e2e')
|
||||
}
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(obj.projectRoot)
|
||||
ctx.lifecycleManager.setCurrentProject(obj.projectRoot)
|
||||
|
||||
// get all the config defaults
|
||||
// and allow us to override them
|
||||
@@ -151,48 +150,47 @@ describe('Routes', () => {
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
this.project = new ProjectBase({ projectRoot: Fixtures.projectPath('e2e'), testingType: 'e2e' })
|
||||
|
||||
cfg.pluginsFile = false
|
||||
|
||||
return Promise.all([
|
||||
return ctx.lifecycleManager.waitForInitializeSuccess()
|
||||
.then(() => {
|
||||
return Promise.all([
|
||||
// open our https server
|
||||
httpsServer.start(8443),
|
||||
httpsServer.start(8443),
|
||||
|
||||
// and open our cypress server
|
||||
(this.server = new ServerE2E()),
|
||||
// and open our cypress server
|
||||
(this.server = new ServerE2E()),
|
||||
|
||||
this.server.open(cfg, {
|
||||
SocketCtor: SocketE2E,
|
||||
getSpec: () => spec,
|
||||
getCurrentBrowser: () => null,
|
||||
createRoutes,
|
||||
testingType: 'e2e',
|
||||
exit: false,
|
||||
})
|
||||
.spread(async (port) => {
|
||||
const automationStub = {
|
||||
use: () => { },
|
||||
}
|
||||
this.server.open(cfg, {
|
||||
SocketCtor: SocketE2E,
|
||||
getSpec: () => spec,
|
||||
getCurrentBrowser: () => null,
|
||||
createRoutes,
|
||||
testingType: 'e2e',
|
||||
exit: false,
|
||||
})
|
||||
.spread(async (port) => {
|
||||
const automationStub = {
|
||||
use: () => { },
|
||||
}
|
||||
|
||||
await this.server.startWebsockets(automationStub, config, {})
|
||||
await this.server.startWebsockets(automationStub, config, {})
|
||||
|
||||
if (initialUrl) {
|
||||
this.server._onDomainSet(initialUrl)
|
||||
}
|
||||
if (initialUrl) {
|
||||
this.server._onDomainSet(initialUrl)
|
||||
}
|
||||
|
||||
this.srv = this.server.getHttpServer()
|
||||
this.srv = this.server.getHttpServer()
|
||||
|
||||
this.session = session(this.srv)
|
||||
this.session = session(this.srv)
|
||||
|
||||
this.proxy = `http://localhost:${port}`
|
||||
}),
|
||||
|
||||
// pluginsModule.init(cfg, {
|
||||
// projectRoot: cfg.projectRoot,
|
||||
// testingType: 'e2e',
|
||||
// }, ctx),
|
||||
])
|
||||
this.proxy = `http://localhost:${port}`
|
||||
}),
|
||||
])
|
||||
})
|
||||
.then(() => {
|
||||
ctx.lifecycleManager.setAndLoadCurrentTestingType('e2e')
|
||||
})
|
||||
}
|
||||
|
||||
if (this.server) {
|
||||
@@ -450,7 +448,7 @@ describe('Routes', () => {
|
||||
let i = 0
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (ctx.lifecycleManager.eventsIpcResult.state === 'loaded') {
|
||||
if (ctx.lifecycleManager.isReady) {
|
||||
clearInterval(interval)
|
||||
resolve()
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('Web Sockets', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.idsPath)
|
||||
|
||||
return config.get(this.idsPath, { port: cyPort })
|
||||
return ctx.lifecycleManager.getFullInitialConfig({ port: cyPort })
|
||||
.then((cfg) => {
|
||||
this.cfg = cfg
|
||||
this.ws = new ws.Server({ port: wsPort })
|
||||
|
||||
@@ -58,23 +58,25 @@ describe('lib/config', () => {
|
||||
|
||||
context('.get', () => {
|
||||
beforeEach(function () {
|
||||
const ctx = getCtx()
|
||||
this.ctx = getCtx()
|
||||
|
||||
this.projectRoot = '/_test-output/path/to/project'
|
||||
|
||||
ctx.lifecycleManager.setCurrentTestingType('e2e')
|
||||
sinon.stub(ctx.lifecycleManager, 'verifyProjectRoot').returns(undefined)
|
||||
sinon.stub(this.ctx.lifecycleManager, 'verifyProjectRoot').returns(undefined)
|
||||
|
||||
this.ctx.lifecycleManager.setCurrentProject(this.projectRoot)
|
||||
this.ctx.lifecycleManager.setCurrentTestingType('e2e')
|
||||
|
||||
this.setup = (cypressJson = {}, cypressEnvJson = {}) => {
|
||||
sinon.stub(ctx.lifecycleManager, 'getConfigFileContents').resolves({ ...cypressJson, e2e: cypressJson.e2e ?? { supportFile: false } })
|
||||
sinon.stub(ctx.lifecycleManager, 'loadCypressEnvFile').resolves(cypressEnvJson)
|
||||
sinon.stub(this.ctx.lifecycleManager._configManager, 'getConfigFileContents').resolves({ ...cypressJson, e2e: cypressJson.e2e ?? { supportFile: false } })
|
||||
sinon.stub(this.ctx.lifecycleManager._configManager, 'reloadCypressEnvFile').resolves(cypressEnvJson)
|
||||
}
|
||||
})
|
||||
|
||||
it('sets projectRoot', function () {
|
||||
this.setup({}, { foo: 'bar' })
|
||||
|
||||
return config.get(this.projectRoot)
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((obj) => {
|
||||
expect(obj.projectRoot).to.eq(this.projectRoot)
|
||||
|
||||
@@ -85,7 +87,7 @@ describe('lib/config', () => {
|
||||
it('sets projectName', function () {
|
||||
this.setup({}, { foo: 'bar' })
|
||||
|
||||
return config.get(this.projectRoot)
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((obj) => {
|
||||
expect(obj.projectName).to.eq('project')
|
||||
})
|
||||
@@ -97,7 +99,7 @@ describe('lib/config', () => {
|
||||
|
||||
this.setup(settings, envSettings)
|
||||
|
||||
return config.get(this.projectRoot)
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then(() => {
|
||||
expect(settings).to.deep.equal({ foo: 'bar' })
|
||||
expect(envSettings).to.deep.equal({ baz: 'qux' })
|
||||
@@ -110,21 +112,21 @@ describe('lib/config', () => {
|
||||
})
|
||||
|
||||
it('can override default port', function () {
|
||||
return config.get(this.projectRoot, { port: 8080 })
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig({ port: 8080 })
|
||||
.then((obj) => {
|
||||
expect(obj.port).to.eq(8080)
|
||||
})
|
||||
})
|
||||
|
||||
it('updates browserUrl', function () {
|
||||
return config.get(this.projectRoot, { port: 8080 })
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig({ port: 8080 })
|
||||
.then((obj) => {
|
||||
expect(obj.browserUrl).to.eq('http://localhost:8080/__/')
|
||||
})
|
||||
})
|
||||
|
||||
it('updates proxyUrl', function () {
|
||||
return config.get(this.projectRoot, { port: 8080 })
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig({ port: 8080 })
|
||||
.then((obj) => {
|
||||
expect(obj.proxyUrl).to.eq('http://localhost:8080')
|
||||
})
|
||||
@@ -134,11 +136,11 @@ describe('lib/config', () => {
|
||||
context('validation', () => {
|
||||
beforeEach(function () {
|
||||
this.expectValidationPasses = () => {
|
||||
return config.get(this.projectRoot) // shouldn't throw
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig() // shouldn't throw
|
||||
}
|
||||
|
||||
this.expectValidationFails = (errorMessage = 'validation error') => {
|
||||
return config.get(this.projectRoot)
|
||||
return this.ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then(() => {
|
||||
throw new Error('should throw validation error')
|
||||
}).catch((err) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require('../spec_helper')
|
||||
|
||||
const files = require('../../lib/files')
|
||||
const config = require('../../lib/config')
|
||||
const FixturesHelper = require('@tooling/system-tests/lib/fixtures')
|
||||
const { getCtx } = require('../../lib/makeDataContext')
|
||||
|
||||
@@ -16,7 +15,7 @@ describe('lib/files', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
|
||||
return config.get(this.todosPath).then((cfg) => {
|
||||
return ctx.lifecycleManager.getFullInitialConfig().then((cfg) => {
|
||||
this.config = cfg;
|
||||
({ projectRoot: this.projectRoot } = cfg)
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.projectRoot)
|
||||
|
||||
@@ -2,7 +2,6 @@ require('../spec_helper')
|
||||
|
||||
const path = require('path')
|
||||
const Promise = require('bluebird')
|
||||
const config = require(`../../lib/config`)
|
||||
const fixture = require(`../../lib/fixture`)
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
const FixturesHelper = require('@tooling/system-tests/lib/fixtures')
|
||||
@@ -28,7 +27,7 @@ describe('lib/fixture', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
|
||||
return config.get(this.todosPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((cfg) => {
|
||||
({ fixturesFolder: this.fixturesFolder } = cfg)
|
||||
})
|
||||
@@ -181,7 +180,7 @@ Expecting 'EOF', '}', ':', ',', ']', got 'STRING'\
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(projectPath)
|
||||
|
||||
return config.get(projectPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((cfg) => {
|
||||
return fixture.get(cfg.fixturesFolder, 'foo')
|
||||
.then((result) => {
|
||||
|
||||
@@ -138,7 +138,7 @@ describe.skip('lib/project-base', () => {
|
||||
const supportFile = 'foo/bar/baz'
|
||||
|
||||
beforeEach(function () {
|
||||
sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar', configFile: 'cypress.config.js' })
|
||||
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').withArgs({ foo: 'bar', configFile: 'cypress.config.js' })
|
||||
.resolves({ baz: 'quux', supportFile, browsers: [] })
|
||||
})
|
||||
|
||||
@@ -176,7 +176,7 @@ describe.skip('lib/project-base', () => {
|
||||
it('does not set cfg.isNewProject when cfg.isTextTerminal', function () {
|
||||
const cfg = { isTextTerminal: true, browsers: [] }
|
||||
|
||||
config.get.resolves(cfg)
|
||||
ctx.lifecycleManager.getFullInitialConfig.resolves(cfg)
|
||||
|
||||
sinon.stub(this.project, '_setSavedState').resolves(cfg)
|
||||
|
||||
@@ -193,7 +193,7 @@ describe.skip('lib/project-base', () => {
|
||||
chromeWebSecurity: false,
|
||||
})
|
||||
|
||||
config.get.restore()
|
||||
ctx.lifecycleManager.getFullInitialConfig.restore()
|
||||
sinon.stub(config, 'get').returns(cfg)
|
||||
|
||||
await this.project.initializeConfig()
|
||||
|
||||
@@ -7,7 +7,6 @@ const { Buffer } = require('buffer')
|
||||
const dataUriToBuffer = require('data-uri-to-buffer')
|
||||
const sizeOf = require('image-size')
|
||||
const Fixtures = require('@tooling/system-tests/lib/fixtures')
|
||||
const config = require(`../../lib/config`)
|
||||
const screenshots = require(`../../lib/screenshots`)
|
||||
const { fs } = require(`../../lib/util/fs`)
|
||||
const plugins = require(`../../lib/plugins`)
|
||||
@@ -62,7 +61,7 @@ describe('lib/screenshots', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
|
||||
return config.get(this.todosPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((config1) => {
|
||||
this.config = config1
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ const socketIo = require('@packages/socket/lib/browser')
|
||||
const httpsAgent = require('https-proxy-agent')
|
||||
|
||||
const errors = require(`../../lib/errors`)
|
||||
const config = require(`../../lib/config`)
|
||||
const { SocketE2E } = require(`../../lib/socket-e2e`)
|
||||
const { ServerE2E } = require(`../../lib/server-e2e`)
|
||||
const { Automation } = require(`../../lib/automation`)
|
||||
@@ -38,7 +37,7 @@ describe('lib/socket', () => {
|
||||
|
||||
ctx.actions.project.setCurrentProjectAndTestingTypeForTestSetup(this.todosPath)
|
||||
|
||||
return config.get(this.todosPath)
|
||||
return ctx.lifecycleManager.getFullInitialConfig()
|
||||
.then((cfg) => {
|
||||
this.cfg = cfg
|
||||
})
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const { devServer } = require('@cypress/webpack-dev-server')
|
||||
|
||||
module.exports = {
|
||||
'projectId': 'abcdef42',
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*',
|
||||
},
|
||||
component: {
|
||||
specPattern: 'cypress/component-tests/**/*',
|
||||
devServer,
|
||||
devServerConfig: {
|
||||
webpackConfig: {
|
||||
output: {
|
||||
publicPath: '/',
|
||||
},
|
||||
},
|
||||
},
|
||||
setupNodeEvents (on, config) {
|
||||
require('@cypress/code-coverage/task')(on, config)
|
||||
|
||||
return config
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
module.exports = {
|
||||
'cypress': {
|
||||
'integrationFolder': 'my-tests',
|
||||
'fileServerFolder': 'dev',
|
||||
e2e: {
|
||||
specPattern: 'my-tests/**/*',
|
||||
supportFile: 'helpers/includes.js',
|
||||
},
|
||||
fileServerFolder: 'dev',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user