Files
cypress/packages/server/lib/open_project.ts
T
Matt Henkes 82429c0027 feat: Defaulting nodeVersion to system (#18732)
* 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
2021-11-04 11:45:25 -07:00

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()