mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-25 08:28:45 -05:00
82429c0027
* Defaulting nodeVersion to system
* try to fix system test
* Rename arg parameters, fix system test in a much better way.
* remove invalid comment
* Add deprecation warning for the nodeVersion config.
* Remove default value to avoid warning regardless of the presence of `nodeVersion`
* More tests fixes 😅
* Updates to deprecation message
* update node version in deprecation notice.
* flex config file name that we tell consumers to update
* simplify validateNoBreakingConfig options
455 lines
12 KiB
TypeScript
455 lines
12 KiB
TypeScript
import _ from 'lodash'
|
|
import la from 'lazy-ass'
|
|
import Debug from 'debug'
|
|
import Bluebird from 'bluebird'
|
|
import chokidar from 'chokidar'
|
|
import pluralize from 'pluralize'
|
|
import { ProjectBase, OpenProjectLaunchOptions } from './project-base'
|
|
import browsers from './browsers'
|
|
import specsUtil from './util/specs'
|
|
import preprocessor from './plugins/preprocessor'
|
|
import runEvents from './plugins/run_events'
|
|
import * as session from './session'
|
|
import { getSpecUrl } from './project_utils'
|
|
import errors from './errors'
|
|
import type { Browser, FoundBrowser, PlatformName } from '@packages/launcher'
|
|
import type { AutomationMiddleware } from './automation'
|
|
|
|
const debug = Debug('cypress:server:open_project')
|
|
|
|
interface LaunchOpts {
|
|
browser?: FoundBrowser
|
|
url?: string
|
|
automationMiddleware?: AutomationMiddleware
|
|
onBrowserClose?: (...args: unknown[]) => void
|
|
onError?: (err: Error) => void
|
|
}
|
|
|
|
interface SpecsByType {
|
|
component: Cypress.Spec[]
|
|
integration: Cypress.Spec[]
|
|
}
|
|
|
|
export interface LaunchArgs {
|
|
_: [string] // Cypress App binary location
|
|
config: Record<string, unknown>
|
|
cwd: string
|
|
browser?: Browser['name']
|
|
configFile?: string
|
|
exit?: boolean
|
|
project: string // projectRoot
|
|
projectRoot: string // same as above
|
|
testingType: Cypress.TestingType
|
|
invokedFromCli: boolean
|
|
os: PlatformName
|
|
userNodePath?: string
|
|
userNodeVersion?: string
|
|
|
|
onFocusTests?: () => any
|
|
/**
|
|
* in run mode, the path of the project run
|
|
* path is relative if specified with --project,
|
|
* absolute if implied by current working directory
|
|
*/
|
|
runProject?: string
|
|
}
|
|
|
|
export class OpenProject {
|
|
openProject: ProjectBase<any> | null = null
|
|
relaunchBrowser: ((...args: unknown[]) => void) | null = null
|
|
specsWatcher: chokidar.FSWatcher | null = null
|
|
componentSpecsWatcher: chokidar.FSWatcher | null = null
|
|
|
|
resetOpenProject () {
|
|
this.openProject = null
|
|
this.relaunchBrowser = null
|
|
}
|
|
|
|
tryToCall (method: keyof ProjectBase<any>) {
|
|
return (...args: unknown[]) => {
|
|
if (this.openProject && this.openProject[method]) {
|
|
return this.openProject[method](...args)
|
|
}
|
|
|
|
return Bluebird.resolve(null)
|
|
}
|
|
}
|
|
|
|
reset () {
|
|
this.resetOpenProject()
|
|
}
|
|
|
|
getConfig () {
|
|
return this.openProject!.getConfig()
|
|
}
|
|
|
|
getRecordKeys = this.tryToCall('getRecordKeys')
|
|
|
|
getRuns = this.tryToCall('getRuns')
|
|
|
|
requestAccess = this.tryToCall('requestAccess')
|
|
|
|
getProject () {
|
|
return this.openProject
|
|
}
|
|
|
|
changeUrlToSpec (spec: Cypress.Cypress['spec']) {
|
|
if (!this.openProject) {
|
|
return
|
|
}
|
|
|
|
const newSpecUrl = getSpecUrl({
|
|
absoluteSpecPath: spec.absolute,
|
|
specType: spec.specType,
|
|
browserUrl: this.openProject.cfg.browserUrl,
|
|
integrationFolder: this.openProject.cfg.integrationFolder || 'integration',
|
|
componentFolder: this.openProject.cfg.componentFolder || 'component',
|
|
projectRoot: this.openProject.projectRoot,
|
|
})
|
|
|
|
this.openProject.changeToUrl(newSpecUrl)
|
|
}
|
|
|
|
launch (browser, spec: Cypress.Cypress['spec'], options: LaunchOpts = {
|
|
onError: () => undefined,
|
|
}) {
|
|
if (!this.openProject) {
|
|
throw Error('Cannot launch runner if openProject is undefined!')
|
|
}
|
|
|
|
debug('resetting project state, preparing to launch browser %s for spec %o options %o',
|
|
browser.name, spec, options)
|
|
|
|
la(_.isPlainObject(browser), 'expected browser object:', browser)
|
|
|
|
// reset to reset server and socket state because
|
|
// of potential domain changes, request buffers, etc
|
|
this.openProject!.reset()
|
|
|
|
const url = getSpecUrl({
|
|
absoluteSpecPath: spec.absolute,
|
|
specType: spec.specType,
|
|
browserUrl: this.openProject.cfg.browserUrl,
|
|
integrationFolder: this.openProject.cfg.integrationFolder || 'integration',
|
|
componentFolder: this.openProject.cfg.componentFolder || 'component?',
|
|
projectRoot: this.openProject.projectRoot,
|
|
})
|
|
|
|
debug('open project url %s', url)
|
|
|
|
const cfg = this.openProject.getConfig()
|
|
|
|
_.defaults(options, {
|
|
browsers: cfg.browsers,
|
|
userAgent: cfg.userAgent,
|
|
proxyUrl: cfg.proxyUrl,
|
|
proxyServer: cfg.proxyServer,
|
|
socketIoRoute: cfg.socketIoRoute,
|
|
chromeWebSecurity: cfg.chromeWebSecurity,
|
|
isTextTerminal: cfg.isTextTerminal,
|
|
downloadsFolder: cfg.downloadsFolder,
|
|
})
|
|
|
|
// if we don't have the isHeaded property
|
|
// then we're in interactive mode and we
|
|
// can assume its a headed browser
|
|
// TODO: we should clean this up
|
|
if (!_.has(browser, 'isHeaded')) {
|
|
browser.isHeaded = true
|
|
browser.isHeadless = false
|
|
}
|
|
|
|
// set the current browser object on options
|
|
// so we can pass it down
|
|
options.browser = browser
|
|
options.url = url
|
|
|
|
this.openProject.setCurrentSpecAndBrowser(spec, browser)
|
|
|
|
const automation = this.openProject.getAutomation()
|
|
|
|
// use automation middleware if its
|
|
// been defined here
|
|
let am = options.automationMiddleware
|
|
|
|
if (am) {
|
|
automation.use(am)
|
|
}
|
|
|
|
if (!am || !am.onBeforeRequest) {
|
|
automation.use({
|
|
onBeforeRequest (message, data) {
|
|
if (message === 'take:screenshot') {
|
|
data.specName = spec.name
|
|
|
|
return data
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
const afterSpec = () => {
|
|
if (!this.openProject || cfg.isTextTerminal || !cfg.experimentalInteractiveRunEvents) {
|
|
return Bluebird.resolve()
|
|
}
|
|
|
|
return runEvents.execute('after:spec', cfg, spec)
|
|
}
|
|
|
|
const { onBrowserClose } = options
|
|
|
|
options.onBrowserClose = () => {
|
|
if (spec && spec.absolute) {
|
|
preprocessor.removeFile(spec.absolute, cfg)
|
|
}
|
|
|
|
afterSpec()
|
|
.catch((err) => {
|
|
this.openProject!.options.onError(err)
|
|
})
|
|
|
|
if (onBrowserClose) {
|
|
return onBrowserClose()
|
|
}
|
|
}
|
|
|
|
options.onError = this.openProject.options.onError
|
|
|
|
this.relaunchBrowser = () => {
|
|
debug(
|
|
'launching browser: %o, spec: %s',
|
|
browser,
|
|
spec.relative,
|
|
)
|
|
|
|
return Bluebird.try(() => {
|
|
if (!cfg.isTextTerminal && cfg.experimentalInteractiveRunEvents) {
|
|
return runEvents.execute('before:spec', cfg, spec)
|
|
}
|
|
|
|
// clear all session data before each spec
|
|
session.clearSessions()
|
|
})
|
|
.then(() => {
|
|
return browsers.open(browser, options, automation)
|
|
})
|
|
}
|
|
|
|
return this.relaunchBrowser()
|
|
}
|
|
|
|
getSpecs (cfg) {
|
|
return specsUtil.findSpecs(cfg)
|
|
.then((specs: Cypress.Spec[] = []) => {
|
|
// TODO merge logic with "run.js"
|
|
if (debug.enabled) {
|
|
const names = _.map(specs, 'name')
|
|
|
|
debug(
|
|
'found %s using spec pattern \'%s\': %o',
|
|
pluralize('spec', names.length, true),
|
|
cfg.testFiles,
|
|
names,
|
|
)
|
|
}
|
|
|
|
const componentTestingEnabled = _.get(cfg, 'resolved.testingType.value', 'e2e') === 'component'
|
|
|
|
if (componentTestingEnabled) {
|
|
// separate specs into integration and component lists
|
|
// note: _.remove modifies the array in place and returns removed elements
|
|
const component = _.remove(specs, { specType: 'component' })
|
|
|
|
return {
|
|
integration: specs,
|
|
component,
|
|
}
|
|
}
|
|
|
|
// assumes all specs are integration specs
|
|
return {
|
|
integration: specs.filter((x) => x.specType === 'integration'),
|
|
component: [],
|
|
}
|
|
})
|
|
}
|
|
|
|
getSpecChanges (options: OpenProjectLaunchOptions = {}) {
|
|
let currentSpecs: SpecsByType
|
|
|
|
_.defaults(options, {
|
|
onChange: () => { },
|
|
onError: () => { },
|
|
})
|
|
|
|
const sendIfChanged = (specs: SpecsByType = { component: [], integration: [] }) => {
|
|
// dont do anything if the specs haven't changed
|
|
if (_.isEqual(specs, currentSpecs)) {
|
|
return
|
|
}
|
|
|
|
currentSpecs = specs
|
|
|
|
return options?.onChange?.(specs)
|
|
}
|
|
|
|
const checkForSpecUpdates = _.debounce(() => {
|
|
if (!this.openProject) {
|
|
return this.stopSpecsWatcher()
|
|
}
|
|
|
|
debug('check for spec updates')
|
|
|
|
return get()
|
|
.then(sendIfChanged)
|
|
.catch(options?.onError)
|
|
}, 250, { leading: true })
|
|
|
|
const createSpecsWatcher = (cfg) => {
|
|
// TODO I keep repeating this to get the resolved value
|
|
// probably better to have a single function that does this
|
|
const componentTestingEnabled = _.get(cfg, 'resolved.testingType.value', 'e2e') === 'component'
|
|
|
|
debug('createSpecWatch component testing enabled', componentTestingEnabled)
|
|
|
|
if (!this.specsWatcher) {
|
|
debug('watching integration test files: %s in %s', cfg.testFiles, cfg.integrationFolder)
|
|
|
|
this.specsWatcher = chokidar.watch(cfg.testFiles, {
|
|
cwd: cfg.integrationFolder,
|
|
ignored: cfg.ignoreTestFiles,
|
|
ignoreInitial: true,
|
|
})
|
|
|
|
this.specsWatcher.on('add', checkForSpecUpdates)
|
|
|
|
this.specsWatcher.on('unlink', checkForSpecUpdates)
|
|
}
|
|
|
|
if (componentTestingEnabled && !this.componentSpecsWatcher) {
|
|
debug('watching component test files: %s in %s', cfg.testFiles, cfg.componentFolder)
|
|
|
|
this.componentSpecsWatcher = chokidar.watch(cfg.testFiles, {
|
|
cwd: cfg.componentFolder,
|
|
ignored: cfg.ignoreTestFiles,
|
|
ignoreInitial: true,
|
|
})
|
|
|
|
this.componentSpecsWatcher.on('add', checkForSpecUpdates)
|
|
|
|
this.componentSpecsWatcher.on('unlink', checkForSpecUpdates)
|
|
}
|
|
}
|
|
|
|
const get = (): Bluebird<SpecsByType> => {
|
|
if (!this.openProject) {
|
|
return Bluebird.resolve({
|
|
component: [],
|
|
integration: [],
|
|
})
|
|
}
|
|
|
|
const cfg = this.openProject.getConfig()
|
|
|
|
createSpecsWatcher(cfg)
|
|
|
|
return this.getSpecs(cfg)
|
|
}
|
|
|
|
// immediately check the first time around
|
|
return checkForSpecUpdates()
|
|
}
|
|
|
|
stopSpecsWatcher () {
|
|
debug('stop spec watcher')
|
|
|
|
if (this.specsWatcher) {
|
|
this.specsWatcher.close()
|
|
this.specsWatcher = null
|
|
}
|
|
|
|
if (this.componentSpecsWatcher) {
|
|
this.componentSpecsWatcher.close()
|
|
this.componentSpecsWatcher = null
|
|
}
|
|
}
|
|
|
|
closeBrowser () {
|
|
return browsers.close()
|
|
}
|
|
|
|
closeOpenProjectAndBrowsers () {
|
|
return this.closeBrowser()
|
|
.then(() => {
|
|
return this.openProject?.close()
|
|
})
|
|
.then(() => {
|
|
this.resetOpenProject()
|
|
|
|
return null
|
|
})
|
|
}
|
|
|
|
close () {
|
|
debug('closing opened project')
|
|
|
|
this.stopSpecsWatcher()
|
|
|
|
return this.closeOpenProjectAndBrowsers()
|
|
}
|
|
|
|
async create (path: string, args: LaunchArgs, options: OpenProjectLaunchOptions) {
|
|
debug('open_project create %s', path)
|
|
|
|
_.defaults(options, {
|
|
onReloadBrowser: () => {
|
|
if (this.relaunchBrowser) {
|
|
return this.relaunchBrowser()
|
|
}
|
|
},
|
|
})
|
|
|
|
if (!_.isUndefined(args.configFile)) {
|
|
options.configFile = args.configFile
|
|
}
|
|
|
|
options = _.extend({}, args.config, options, { args })
|
|
|
|
// open the project and return
|
|
// the config for the project instance
|
|
debug('opening project %s', path)
|
|
debug('and options %o', options)
|
|
|
|
// store the currently open project
|
|
this.openProject = new ProjectBase({
|
|
testingType: args.testingType === 'component' ? 'component' : 'e2e',
|
|
projectRoot: path,
|
|
options: {
|
|
...options,
|
|
testingType: args.testingType,
|
|
},
|
|
})
|
|
|
|
try {
|
|
await this.openProject.initializeConfig()
|
|
await this.openProject.open()
|
|
} catch (err: any) {
|
|
if (err.isCypressErr && err.portInUse) {
|
|
errors.throw(err.type, err.port)
|
|
} else {
|
|
// rethrow and handle elsewhere
|
|
throw (err)
|
|
}
|
|
}
|
|
|
|
return this
|
|
}
|
|
|
|
// for testing purposes
|
|
__reset () {
|
|
this.resetOpenProject()
|
|
}
|
|
}
|
|
|
|
export const openProject = new OpenProject()
|