chore(server): reduce coupling between methods to instantiate a project (#17200)

* wip

* wip: use async/await

* slowly refactor

* slowly refactor

* use typescript

* use async/await

* update spec

* do not use tap because bluebird sucks

* refactor

* update

* remove bluebird

* update

* remove then

* change to async/await to make it easier to reason about

* remove more bluebird

* no more bluebird

* remov ts ignore

* do not tap

* refactor

* simplify cfg

* move static methods to separate file

* update snapshot

* comment out test

* simplifying options merging

* update tests

* change order of options

* move code out of project-base

* update tests

* update tests

* type reporter

* simplify onWatchSettings function

* sep starting websockets and watching settings. reduce need to pass large cfg object

* move util functions out of project class

* move tests to new file

* update test

* move code around

* update tests

* remove need to pass options to getConfig

* fix tests

* separate get and init config

* set browser warnings in initializeConfig

* move Ct specific concerns to same function

* do not pass config to initializeSpecStore

* remove onOpen function

* improve types

* update typing errors

* update types

* types

* fix types

* update tests

* update tests

* fix tests

* update tests

* comment back in test

* update methods

* update types

* add defensive code against config.clientRoute

* do not use async

* update tests

* use same baseUrl for proxy regardless

* remove comment

* revert change to baseUrl
This commit is contained in:
Lachlan Miller
2021-07-13 11:32:59 +10:00
committed by GitHub
parent bda59dd7cc
commit b945b8197e
24 changed files with 1422 additions and 1166 deletions
+1
View File
@@ -2763,6 +2763,7 @@ declare namespace Cypress {
clientRoute: string
configFile: string
cypressEnv: string
devServerPublicPathRoute: string
isNewProject: boolean
isTextTerminal: boolean
morgan: boolean
@@ -20,7 +20,6 @@ export async function start ({ webpackConfig: userWebpackConfig, template, optio
debug('User did not pass in any webpack configuration')
}
// @ts-expect-error ?? devServerPublicPathRoute is not a valid option of Cypress.Config
const { projectRoot, devServerPublicPathRoute, isTextTerminal } = options.config
const webpackConfig = await makeWebpackConfig(userWebpackConfig || {}, {
+11 -5
View File
@@ -6,7 +6,7 @@ import { NetworkProxy } from '@packages/proxy'
import { handle, serve, serveChunk } from './runner-ct'
import xhrs from '@packages/server/lib/controllers/xhrs'
import { SpecsStore } from '@packages/server/lib/specs-store'
import { ProjectBase } from '../../server/lib/project-base'
import { Cfg, ProjectBase } from '../../server/lib/project-base'
import { getPathToDist } from '@packages/resolve-dist'
const debug = Debug('cypress:server:routes')
@@ -14,7 +14,7 @@ const debug = Debug('cypress:server:routes')
export interface InitializeRoutes {
app: Express
specsStore: SpecsStore
config: Record<string, any>
config: Cfg
project: ProjectBase<any>
nodeProxy: httpProxy
networkProxy: NetworkProxy
@@ -73,11 +73,17 @@ export const createRoutes = ({
})
})
const clientRoute = config.clientRoute
if (!clientRoute) {
throw Error(`clientRoute is required. Received ${clientRoute}`)
}
app.all('/__cypress/xhrs/*', (req, res, next) => {
xhrs.handle(req, res, config, next)
})
app.get(config.clientRoute, (req, res) => {
app.get(clientRoute, (req, res) => {
debug('Serving Cypress front-end by requested URL:', req.url)
serve(req, res, {
@@ -88,13 +94,13 @@ export const createRoutes = ({
})
// enables runner-ct to make a dynamic import
app.get(`${config.clientRoute}ctChunk-*`, (req, res) => {
app.get(`${clientRoute}ctChunk-*`, (req, res) => {
debug('Serving Cypress front-end chunk by requested URL:', req.url)
serveChunk(req, res, { config })
})
app.get(`${config.clientRoute}vendors~ctChunk-*`, (req, res) => {
app.get(`${clientRoute}vendors~ctChunk-*`, (req, res) => {
debug('Serving Cypress front-end vendor chunk by requested URL:', req.url)
serveChunk(req, res, { config })
+2 -1
View File
@@ -3,11 +3,12 @@ import httpsProxy from '@packages/https-proxy'
import { OpenServerOptions, ServerBase } from '@packages/server/lib/server-base'
import appData from '@packages/server/lib/util/app_data'
import { SocketCt } from './socket-ct'
import { Cfg } from '../../server/lib/project-base'
type WarningErr = Record<string, any>
export class ServerCt extends ServerBase<SocketCt> {
open (config: Record<string, any> = {}, options: OpenServerOptions) {
open (config: Cfg, options: OpenServerOptions) {
return super.open(config, { ...options, projectType: 'ct' })
}
@@ -12,6 +12,7 @@ Cannot find module '/foo/bar/.projects/e2e/node_modules/module-does-not-exist'
Require stack:
- lib/reporter.js
- lib/project-base.ts
- lib/project_static.ts
- lib/modes/run.js
- lib/modes/run-e2e.js
- lib/modes/index.js
+1 -1
View File
@@ -22,7 +22,7 @@ export class Automation {
private cookies: Cookies
private screenshot: { capture: (data: any, automate: any) => any }
constructor (cyNamespace: string, cookieNamespace: string, screenshotsFolder: string, public onBrowserPreRequest: OnBrowserPreRequest) {
constructor (cyNamespace?: string, cookieNamespace?: string, screenshotsFolder?: string | false, public onBrowserPreRequest?: OnBrowserPreRequest) {
this.requests = {}
// set the middleware
+1 -1
View File
@@ -1,6 +1,6 @@
import screenshots from '../screenshots'
export function Screenshot (screenshotsFolder: string) {
export function Screenshot (screenshotsFolder?: string | false) {
return {
capture (data, automate) {
return screenshots.capture(data, automate)
@@ -179,7 +179,7 @@ export class CdpAutomation {
originalResourceType: params.type,
}
this.automation.onBrowserPreRequest(browserPreRequest)
this.automation.onBrowserPreRequest?.(browserPreRequest)
}
private getAllCookies = (filter: CyCookieFilter) => {
+11 -10
View File
@@ -16,7 +16,8 @@ const open = require('../util/open')
const user = require('../user')
const errors = require('../errors')
const Updater = require('../updater')
const { ProjectBase } = require('../project-base')
const ProjectStatic = require('../project_static')
const openProject = require('../open_project')
const ensureUrl = require('../util/ensure-url')
const chromePolicyCheck = require('../util/chrome_policy_check')
@@ -236,37 +237,37 @@ const handleEvent = function (options, bus, event, id, type, arg) {
return send(null)
case 'get:orgs':
return ProjectBase.getOrgs()
return ProjectStatic.getOrgs()
.then(send)
.catch(sendErr)
case 'get:projects':
return ProjectBase.getPathsAndIds()
return ProjectStatic.getPathsAndIds()
.then(send)
.catch(sendErr)
case 'get:project:statuses':
return ProjectBase.getProjectStatuses(arg)
return ProjectStatic.getProjectStatuses(arg)
.then(send)
.catch(sendErr)
case 'get:project:status':
return ProjectBase.getProjectStatus(arg)
return ProjectStatic.getProjectStatus(arg)
.then(send)
.catch(sendErr)
case 'get:dashboard:projects':
return ProjectBase.getDashboardProjects()
return ProjectStatic.getDashboardProjects()
.then(send)
.catch(sendErr)
case 'add:project':
return ProjectBase.add(arg, options)
return ProjectStatic.add(arg, options)
.then(send)
.catch(sendErr)
case 'remove:project':
return ProjectBase.remove(arg)
return ProjectStatic.remove(arg)
.then(() => {
return send(arg)
})
@@ -331,12 +332,12 @@ const handleEvent = function (options, bus, event, id, type, arg) {
.catch(sendErr)
case 'setup:dashboard:project':
return openProject.createCiProject(arg)
return ProjectStatic.createCiProject(arg, options.projectRoot)
.then(send)
.catch(sendErr)
case 'set:project:id':
return openProject.writeProjectId(arg)
return ProjectStatic.writeProjectId(arg, options.projectRoot)
.then(send)
.catch(sendErr)
+9 -18
View File
@@ -11,7 +11,7 @@ const logSymbols = require('log-symbols')
const recordMode = require('./record')
const errors = require('../errors')
const { ProjectBase } = require('../project-base')
const ProjectStatic = require('../project_static')
const Reporter = require('../reporter')
const browserUtils = require('../browsers')
const openProject = require('../open_project')
@@ -606,20 +606,13 @@ const openProjectCreate = (projectRoot, socketId, args) => {
onError: args.onError,
}
return openProject
.create(projectRoot, args, options)
.catch({ portInUse: true }, (err) => {
// TODO: this needs to move to call exitEarly
// so we record the failure in CI
return errors.throw('PORT_IN_USE_LONG', err.port)
})
return openProject.create(projectRoot, args, options)
}
const createAndOpenProject = function (socketId, options) {
const { projectRoot, projectId } = options
return ProjectBase
.ensureExists(projectRoot, options)
return ProjectStatic.ensureExists(projectRoot, options)
.then(() => {
// open this project without
// adding it to the global cache
@@ -973,10 +966,14 @@ module.exports = {
return project.onWarning
}
debug('browser launched')
return openProject.launch(browser, spec, browserOpts)
},
navigateToNextSpec (spec) {
debug('navigating to next spec')
return openProject.changeUrlToSpec(spec)
},
@@ -1083,10 +1080,7 @@ module.exports = {
// If we do not launch the browser,
// we tell it that we are ready
// to receive the next spec
return this.navigateToNextSpec(options.spec)
.tap(() => {
debug('navigated to next spec')
})
return Promise.resolve(this.navigateToNextSpec(options.spec))
}
return Promise.join(
@@ -1094,10 +1088,7 @@ module.exports = {
.tap(() => {
debug('socket connected', { socketId })
}),
this.launchBrowser(options)
.tap(() => {
debug('browser launched')
}),
this.launchBrowser(options),
)
.timeout(browserTimeout)
.catch(Promise.TimeoutError, (err) => {
+135 -110
View File
@@ -9,6 +9,8 @@ const browsers = require('./browsers')
const specsUtil = require('./util/specs')
const preprocessor = require('./plugins/preprocessor')
const runEvents = require('./plugins/run_events')
const { getSpecUrl } = require('./project_utils')
const errors = require('./errors')
const moduleFactory = () => {
let openProject = null
@@ -34,29 +36,31 @@ const moduleFactory = () => {
componentSpecsWatcher: null,
reset: tryToCall('reset'),
reset: () => openProject?.reset(),
getConfig: tryToCall('getConfig'),
createCiProject: tryToCall('createCiProject'),
writeProjectId: tryToCall('writeProjectId'),
getRecordKeys: tryToCall('getRecordKeys'),
getRuns: tryToCall('getRuns'),
requestAccess: tryToCall('requestAccess'),
emit: tryToCall('emit'),
getProject () {
return openProject
},
changeUrlToSpec (spec) {
return openProject.getSpecUrl(spec.absolute, spec.specType)
.then((newSpecUrl) => openProject.changeToUrl(newSpecUrl))
const newSpecUrl = getSpecUrl({
absoluteSpecPath: spec.absolute,
specType: spec.specType,
browserUrl: openProject.cfg.browserUrl,
integrationFolder: openProject.cfg.integrationFolder,
componentFolder: openProject.cfg.componentFolder,
projectRoot: openProject.projectRoot,
})
openProject.changeToUrl(newSpecUrl)
},
launch (browser, spec, options = {}) {
@@ -67,106 +71,113 @@ const moduleFactory = () => {
// reset to reset server and socket state because
// of potential domain changes, request buffers, etc
return this.reset()
.then(() => openProject.getSpecUrl(spec.absolute, spec.specType))
.then((url) => {
debug('open project url %s', url)
this.reset()
return openProject.getConfig()
.then((cfg) => {
_.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,
const url = getSpecUrl({
absoluteSpecPath: spec.absolute,
specType: spec.specType,
browserUrl: openProject.cfg.browserUrl,
integrationFolder: openProject.cfg.integrationFolder,
componentFolder: openProject.cfg.componentFolder,
projectRoot: openProject.projectRoot,
})
debug('open project url %s', url)
return openProject.getConfig()
.then((cfg) => {
_.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
openProject.setCurrentSpecAndBrowser(spec, browser)
const automation = 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 (!openProject || cfg.isTextTerminal || !cfg.experimentalInteractiveRunEvents) return Promise.resolve()
return runEvents.execute('after:spec', cfg, spec)
}
const { onBrowserClose } = options
options.onBrowserClose = () => {
if (spec && spec.absolute) {
preprocessor.removeFile(spec.absolute, cfg)
}
afterSpec(cfg, spec)
.catch((err) => {
openProject.options.onError(err)
})
// 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
if (onBrowserClose) {
return onBrowserClose()
}
}
// set the current browser object on options
// so we can pass it down
options.browser = browser
options.url = url
options.onError = openProject.options.onError
openProject.setCurrentSpecAndBrowser(spec, browser)
relaunchBrowser = () => {
debug(
'launching browser: %o, spec: %s',
browser,
spec.relative,
)
const automation = 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 (!openProject || cfg.isTextTerminal || !cfg.experimentalInteractiveRunEvents) return Promise.resolve()
return runEvents.execute('after:spec', cfg, spec)
}
const { onBrowserClose } = options
options.onBrowserClose = () => {
if (spec && spec.absolute) {
preprocessor.removeFile(spec.absolute, cfg)
return Promise.try(() => {
if (!cfg.isTextTerminal && cfg.experimentalInteractiveRunEvents) {
return runEvents.execute('before:spec', cfg, spec)
}
})
.then(() => {
return browsers.open(browser, options, automation)
})
}
afterSpec(cfg, spec)
.catch((err) => {
openProject.options.onError(err)
})
if (onBrowserClose) {
return onBrowserClose()
}
}
options.onError = openProject.options.onError
relaunchBrowser = () => {
debug(
'launching browser: %o, spec: %s',
browser,
spec.relative,
)
return Promise.try(() => {
if (!cfg.isTextTerminal && cfg.experimentalInteractiveRunEvents) {
return runEvents.execute('before:spec', cfg, spec)
}
})
.then(() => {
return browsers.open(browser, options, automation)
})
}
return relaunchBrowser()
})
return relaunchBrowser()
})
},
@@ -323,15 +334,8 @@ const moduleFactory = () => {
return this.closeOpenProjectAndBrowsers()
},
create (path, args = {}, options = {}) {
async create (path, args = {}, options = {}) {
debug('open_project create %s', path)
debug('and options %o', options)
// store the currently open project
openProject = new Project.ProjectBase({
projectType: args.testingType === 'component' ? 'ct' : 'e2e',
projectRoot: path,
})
_.defaults(options, {
onReloadBrowser: () => {
@@ -352,8 +356,29 @@ const moduleFactory = () => {
debug('opening project %s', path)
debug('and options %o', options)
return openProject.open({ ...options, testingType: args.testingType })
.return(this)
// store the currently open project
openProject = new Project.ProjectBase({
projectType: args.testingType === 'component' ? 'ct' : 'e2e',
projectRoot: path,
options: {
...options,
testingType: args.testingType,
},
})
try {
await openProject.initializeConfig()
await openProject.open()
} catch (err) {
if (err.isCypressErr && err.portInUse) {
errors.throw(err.type, err.port)
} else {
// rethrow and handle elsewhere
throw (err)
}
}
return this
},
// for testing purposes
File diff suppressed because it is too large Load Diff
+200
View File
@@ -0,0 +1,200 @@
import Debug from 'debug'
import commitInfo from '@cypress/commit-info'
import _ from 'lodash'
import logger from './logger'
import api from './api'
import cache from './cache'
import user from './user'
import keys from './util/keys'
import settings from './util/settings'
import { ProjectBase } from './project-base'
const debug = Debug('cypress:server:project_static')
export async function getOrgs () {
const authToken = await user.ensureAuthToken()
return api.getOrgs(authToken)
}
export function paths () {
return cache.getProjectRoots()
}
export async function getPathsAndIds () {
const projectRoots: string[] = await cache.getProjectRoots()
// this assumes that the configFile for a cached project is 'cypress.json'
// https://git.io/JeGyF
return Promise.all(projectRoots.map(async (projectRoot) => {
return {
path: projectRoot,
id: await settings.id(projectRoot),
}
}))
}
export async function getDashboardProjects () {
const authToken = await user.ensureAuthToken()
debug('got auth token: %o', { authToken: keys.hide(authToken) })
return api.getProjects(authToken)
}
export function _mergeDetails (clientProject, project) {
return _.extend({}, clientProject, project, { state: 'VALID' })
}
export function _mergeState (clientProject, state) {
return _.extend({}, clientProject, { state })
}
export async function _getProject (clientProject, authToken) {
debug('get project from api', clientProject.id, clientProject.path)
try {
const project = await api.getProject(clientProject.id, authToken)
debug('got project from api')
return _mergeDetails(clientProject, project)
} catch (err) {
debug('failed to get project from api', err.statusCode)
switch (err.statusCode) {
case 404:
// project doesn't exist
return _mergeState(clientProject, 'INVALID')
case 403:
// project exists, but user isn't authorized for it
return _mergeState(clientProject, 'UNAUTHORIZED')
default:
throw err
}
}
}
export async function getProjectStatuses (clientProjects: any = []) {
debug(`get project statuses for ${clientProjects.length} projects`)
const authToken = await user.ensureAuthToken()
debug('got auth token: %o', { authToken: keys.hide(authToken) })
const projects = (await api.getProjects(authToken) || [])
debug(`got ${projects.length} projects`)
const projectsIndex = _.keyBy(projects, 'id')
return Promise.all(_.map(clientProjects, (clientProject) => {
debug('looking at', clientProject.path)
// not a CI project, just mark as valid and return
if (!clientProject.id) {
debug('no project id')
return _mergeState(clientProject, 'VALID')
}
const project = projectsIndex[clientProject.id]
if (project) {
debug('found matching:', project)
// merge in details for matching project
return _mergeDetails(clientProject, project)
}
debug('did not find matching:', project)
// project has id, but no matching project found
// check if it doesn't exist or if user isn't authorized
return _getProject(clientProject, authToken)
}))
}
export async function getProjectStatus (clientProject) {
debug('get project status for client id %s at path %s', clientProject.id, clientProject.path)
if (!clientProject.id) {
debug('no project id')
return Promise.resolve(_mergeState(clientProject, 'VALID'))
}
const authToken = await user.ensureAuthToken()
debug('got auth token: %o', { authToken: keys.hide(authToken) })
return _getProject(clientProject, authToken)
}
export function remove (path) {
return cache.removeProject(path)
}
export async function add (path, options) {
// don't cache a project if a non-default configFile is set
// https://git.io/JeGyF
if (settings.configFile(options) !== 'cypress.json') {
return Promise.resolve({ path })
}
try {
await cache.insertProject(path)
const id = await getId(path)
return {
id,
path,
}
} catch (e) {
return { path }
}
}
export function getId (path) {
return new ProjectBase({ projectRoot: path, projectType: 'e2e', options: {} }).getProjectId()
}
export function ensureExists (path, options) {
// is there a configFile? is the root writable?
return settings.exists(path, options)
}
export async function writeProjectId (id: string, projectRoot: string) {
const attrs = { projectId: id }
logger.info('Writing Project ID', _.clone(attrs))
// TODO: We need to set this
// this.generatedProjectIdTimestamp = new Date()
await settings.write(projectRoot, attrs)
return id
}
interface ProjectDetails {
projectName: string
orgId: string | null
public: boolean
}
export async function createCiProject (projectDetails: ProjectDetails, projectRoot: string) {
debug('create CI project with projectDetails %o projectRoot %s', projectDetails, projectRoot)
const authToken = await user.ensureAuthToken()
const remoteOrigin = await commitInfo.getRemoteOrigin(projectRoot)
debug('found remote origin at projectRoot %o', {
remoteOrigin,
projectRoot,
})
const newProject = await api.createProject(projectDetails, remoteOrigin, authToken)
await writeProjectId(newProject.id, projectRoot)
return newProject
}
+132
View File
@@ -0,0 +1,132 @@
import Debug from 'debug'
import path from 'path'
import settings from './util/settings'
import errors from './errors'
import { fs } from './util/fs'
import { escapeFilenameInUrl } from './util/escape_filename'
const debug = Debug('cypress:server:project_utils')
const multipleForwardSlashesRe = /[^:\/\/](\/{2,})/g
const backSlashesRe = /\\/g
const normalizeSpecUrl = (browserUrl: string, specUrl: string) => {
const replacer = (match: string) => match.replace('//', '/')
return [
browserUrl,
'#/tests',
escapeFilenameInUrl(specUrl),
].join('/')
.replace(multipleForwardSlashesRe, replacer)
}
const getPrefixedPathToSpec = ({
integrationFolder,
componentFolder,
projectRoot,
type,
pathToSpec,
}: {
integrationFolder: string
componentFolder: string
projectRoot: string
type: string
pathToSpec: string
}) => {
type ??= 'integration'
// for now hard code the 'type' as integration
// but in the future accept something different here
// strip out the integration folder and prepend with "/"
// example:
//
// /Users/bmann/Dev/cypress-app/.projects/cypress/integration
// /Users/bmann/Dev/cypress-app/.projects/cypress/integration/foo.js
//
// becomes /integration/foo.js
const folderToUse = type === 'integration' ? integrationFolder : componentFolder
// To avoid having invalid urls from containing backslashes,
// we normalize specUrls to posix by replacing backslash by slash
// Indeed, path.realtive will return something different on windows
// than on posix systems which can lead to problems
const url = `/${path.join(type, path.relative(
folderToUse,
path.resolve(projectRoot, pathToSpec),
)).replace(backSlashesRe, '/')}`
debug('prefixed path for spec %o', { pathToSpec, type, url })
return url
}
export const getSpecUrl = ({
absoluteSpecPath,
specType,
browserUrl,
integrationFolder,
componentFolder,
projectRoot,
}: {
absoluteSpecPath?: string
browserUrl: string
integrationFolder: string
componentFolder: string
projectRoot: string
specType: 'integration' | 'component'
}) => {
specType ??= 'integration'
debug('get spec url: %s for spec type %s', absoluteSpecPath, specType)
// if we don't have a absoluteSpecPath or its __all
if (!absoluteSpecPath || (absoluteSpecPath === '__all')) {
const url = normalizeSpecUrl(browserUrl, '/__all')
debug('returning url to run all specs: %s', url)
return url
}
// TODO:
// to handle both unit + integration tests we need
// to figure out (based on the config) where this absoluteSpecPath
// lives. does it live in the integrationFolder or
// the unit folder?
// once we determine that we can then prefix it correctly
// with either integration or unit
const prefixedPath = getPrefixedPathToSpec({
integrationFolder,
componentFolder,
projectRoot,
pathToSpec: absoluteSpecPath,
type: specType,
})
const url = normalizeSpecUrl(browserUrl, prefixedPath)
debug('return path to spec %o', { specType, absoluteSpecPath, prefixedPath, url })
return url
}
export const checkSupportFile = async ({
supportFile,
configFile,
}: {
supportFile?: string | boolean
configFile?: string | boolean
}) => {
if (supportFile && typeof supportFile === 'string') {
const found = await fs.pathExists(supportFile)
if (!found) {
errors.throw('SUPPORT_FILE_NOT_FOUND', supportFile, settings.configFile({ configFile }))
}
}
return
}
+1 -1
View File
@@ -98,7 +98,7 @@ export const createRoutes = ({
la(check.unemptyString(config.clientRoute), 'missing client route in config', config)
app.get(config.clientRoute, (req, res) => {
app.get(`${config.clientRoute}`, (req, res) => {
debug('Serving Cypress front-end by requested URL:', req.url)
runner.serve(req, res, {
+5 -6
View File
@@ -27,7 +27,7 @@ import { SocketAllowed } from './util/socket_allowed'
import { createInitialWorkers } from '@packages/rewriter'
import { RunnerType, SpecsStore } from './specs-store'
import { InitializeRoutes } from '../../server-ct/src/routes-ct'
import { ProjectBase } from './project-base'
import { Cfg, ProjectBase } from './project-base'
const ALLOWED_PROXY_BYPASS_URLS = [
'/',
@@ -161,13 +161,13 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
abstract createServer (
app: Express,
config: Record<string, any>,
config: Cfg,
project: ProjectBase<any>,
request: unknown,
onWarning: unknown,
): Bluebird<[number, WarningErr?]>
open (config: Record<string, any> = {}, {
open (config: Cfg, {
project,
onError,
onWarning,
@@ -190,9 +190,8 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
logger.setSettings(config)
// TODO: Can we just pass config.baseUrl regardless of project type?
this._nodeProxy = httpProxy.createProxyServer({
target: projectType === 'ct' ? config.baseUrl : undefined,
target: config.baseUrl && projectType === 'ct' ? config.baseUrl : undefined,
})
this._socket = new SocketCtor(config) as TSocket
@@ -316,7 +315,7 @@ export abstract class ServerBase<TSocket extends SocketE2E | SocketCt> {
return io
}
createHosts (hosts = {}) {
createHosts (hosts: string[] | null = []) {
return _.each(hosts, (ip, host) => {
return evilDns.add(host, ip)
})
+2 -1
View File
@@ -15,6 +15,7 @@ import appData from './util/app_data'
import * as ensureUrl from './util/ensure-url'
import headersUtil from './util/headers'
import statusCode from './util/status_code'
import { Cfg } from './project-base'
type WarningErr = Record<string, any>
@@ -49,7 +50,7 @@ export class ServerE2E extends ServerBase<SocketE2E> {
this._urlResolver = null
}
open (config: Record<string, any> = {}, options: OpenServerOptions) {
open (config: Cfg, options: OpenServerOptions) {
return super.open(config, { ...options, projectType: 'e2e' })
}
@@ -33,6 +33,7 @@ const errors = require(`${root}lib/errors`)
const plugins = require(`${root}lib/plugins`)
const cypress = require(`${root}lib/cypress`)
const ProjectBase = require(`${root}lib/project-base`).ProjectBase
const { getId } = require(`${root}lib/project_static`)
const { ServerE2E } = require(`${root}lib/server-e2e`)
const Reporter = require(`${root}lib/reporter`)
const Watchers = require(`${root}lib/watchers`)
@@ -1110,7 +1111,8 @@ describe('lib/cypress', () => {
})
// TODO: handle PORT_IN_USE short integration test
it('logs error and exits when port is in use', function () {
it('logs error and exits when port is in use', async function () {
sinon.stub(ProjectBase.prototype, 'getAutomation').returns({ use: () => {} })
let server = http.createServer()
server = Promise.promisifyAll(server)
@@ -1119,7 +1121,7 @@ describe('lib/cypress', () => {
.then(() => {
return cypress.start([`--run-project=${this.todosPath}`, '--port=5544'])
}).then(() => {
this.expectExitWithErr('PORT_IN_USE_LONG', '5544')
this.expectExitWithErr('PORT_IN_USE_SHORT', '5544')
})
})
})
@@ -1235,7 +1237,7 @@ describe('lib/cypress', () => {
// make sure we have no user object
user.set({}),
ProjectBase.id(this.todosPath)
getId(this.todosPath)
.then((id) => {
this.projectId = id
}),
@@ -1676,7 +1678,6 @@ describe('lib/cypress', () => {
})
it('passes filtered options to Project#open and sets cli config', function () {
const getConfig = sinon.spy(ProjectBase.prototype, 'getConfig')
const open = sinon.stub(ServerE2E.prototype, 'open').resolves([])
process.env.CYPRESS_FILE_SERVER_FOLDER = 'foo'
@@ -1706,12 +1707,12 @@ describe('lib/cypress', () => {
return Events.handleEvent(options, {}, {}, 123, 'open:project', this.todosPath)
}).then(() => {
expect(getConfig).to.be.calledWithMatch({
port: 2121,
pageLoadTimeout: 1000,
report: false,
env: { baz: 'baz' },
})
const projectOptions = openProject.getProject().options
expect(projectOptions.port).to.eq(2121)
expect(projectOptions.pageLoadTimeout).to.eq(1000)
expect(projectOptions.report).to.eq(false)
expect(projectOptions.env).to.eql({ baz: 'baz' })
expect(open).to.be.called
+53 -43
View File
@@ -9,6 +9,7 @@ const chromePolicyCheck = require(`${root}../lib/util/chrome_policy_check`)
const cache = require(`${root}../lib/cache`)
const logger = require(`${root}../lib/logger`)
const ProjectBase = require(`${root}../lib/project-base`).ProjectBase
const ProjectStatic = require(`${root}../lib/project_static`)
const Updater = require(`${root}../lib/updater`)
const user = require(`${root}../lib/user`)
const errors = require(`${root}../lib/errors`)
@@ -49,12 +50,12 @@ describe('lib/gui/events', () => {
sinon.stub(electron.ipcMain, 'on')
sinon.stub(electron.ipcMain, 'removeAllListeners')
this.handleEvent = (type, arg) => {
this.handleEvent = (type, arg, bus = this.bus) => {
const id = `${type}-${Math.random()}`
return Promise
.try(() => {
return events.handleEvent(this.options, this.bus, this.event, id, type, arg)
return events.handleEvent(this.options, bus, this.event, id, type, arg)
}).return({
sendCalledWith: (data) => {
expect(this.send).to.be.calledWith('response', { id, data })
@@ -442,7 +443,7 @@ describe('lib/gui/events', () => {
context('user events', () => {
describe('get:orgs', () => {
it('returns array of orgs', function () {
sinon.stub(ProjectBase, 'getOrgs').resolves([])
sinon.stub(ProjectStatic, 'getOrgs').resolves([])
return this.handleEvent('get:orgs').then((assert) => {
return assert.sendCalledWith([])
@@ -452,7 +453,7 @@ describe('lib/gui/events', () => {
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(ProjectBase, 'getOrgs').rejects(err)
sinon.stub(ProjectStatic, 'getOrgs').rejects(err)
return this.handleEvent('get:orgs').then((assert) => {
return assert.sendErrCalledWith(err)
@@ -547,7 +548,7 @@ describe('lib/gui/events', () => {
context('project events', () => {
describe('get:projects', () => {
it('returns array of projects', function () {
sinon.stub(ProjectBase, 'getPathsAndIds').resolves([])
sinon.stub(ProjectStatic, 'getPathsAndIds').resolves([])
return this.handleEvent('get:projects').then((assert) => {
return assert.sendCalledWith([])
@@ -557,7 +558,7 @@ describe('lib/gui/events', () => {
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(ProjectBase, 'getPathsAndIds').rejects(err)
sinon.stub(ProjectStatic, 'getPathsAndIds').rejects(err)
return this.handleEvent('get:projects').then((assert) => {
return assert.sendErrCalledWith(err)
@@ -567,7 +568,7 @@ describe('lib/gui/events', () => {
describe('get:project:statuses', () => {
it('returns array of projects with statuses', function () {
sinon.stub(ProjectBase, 'getProjectStatuses').resolves([])
sinon.stub(ProjectStatic, 'getProjectStatuses').resolves([])
return this.handleEvent('get:project:statuses').then((assert) => {
return assert.sendCalledWith([])
@@ -577,7 +578,7 @@ describe('lib/gui/events', () => {
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(ProjectBase, 'getProjectStatuses').rejects(err)
sinon.stub(ProjectStatic, 'getProjectStatuses').rejects(err)
return this.handleEvent('get:project:statuses').then((assert) => {
return assert.sendErrCalledWith(err)
@@ -587,7 +588,7 @@ describe('lib/gui/events', () => {
describe('get:project:status', () => {
it('returns project returned by Project.getProjectStatus', function () {
sinon.stub(ProjectBase, 'getProjectStatus').resolves('project')
sinon.stub(ProjectStatic, 'getProjectStatus').resolves('project')
return this.handleEvent('get:project:status').then((assert) => {
return assert.sendCalledWith('project')
@@ -597,7 +598,7 @@ describe('lib/gui/events', () => {
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(ProjectBase, 'getProjectStatus').rejects(err)
sinon.stub(ProjectStatic, 'getProjectStatus').rejects(err)
return this.handleEvent('get:project:status').then((assert) => {
return assert.sendErrCalledWith(err)
@@ -607,7 +608,7 @@ describe('lib/gui/events', () => {
describe('add:project', () => {
it('adds project + returns result', function () {
sinon.stub(ProjectBase, 'add').withArgs('/_test-output/path/to/project', this.options).resolves('result')
sinon.stub(ProjectStatic, 'add').withArgs('/_test-output/path/to/project', this.options).resolves('result')
return this.handleEvent('add:project', '/_test-output/path/to/project').then((assert) => {
return assert.sendCalledWith('result')
@@ -617,7 +618,7 @@ describe('lib/gui/events', () => {
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(ProjectBase, 'add').withArgs('/_test-output/path/to/project', this.options).rejects(err)
sinon.stub(ProjectStatic, 'add').withArgs('/_test-output/path/to/project', this.options).rejects(err)
return this.handleEvent('add:project', '/_test-output/path/to/project').then((assert) => {
return assert.sendErrCalledWith(err)
@@ -646,11 +647,19 @@ describe('lib/gui/events', () => {
})
describe('open:project', () => {
function busStub () {
return {
on: sinon.stub(),
removeAllListeners: sinon.stub(),
}
}
beforeEach(function () {
sinon.stub(extension, 'setHostAndPath').resolves()
sinon.stub(browsers, 'getAllBrowsersWith')
browsers.getAllBrowsersWith.resolves([])
browsers.getAllBrowsersWith.withArgs('/usr/bin/baz-browser').resolves([{ foo: 'bar' }])
this.initializeConfig = sinon.stub(ProjectBase.prototype, 'initializeConfig').resolves()
this.open = sinon.stub(ProjectBase.prototype, 'open').resolves()
sinon.stub(ProjectBase.prototype, 'close').resolves()
@@ -664,7 +673,9 @@ describe('lib/gui/events', () => {
it('open project + returns config', function () {
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then((assert) => {
return assert.sendCalledWith({ some: 'config' })
expect(this.send.firstCall.args[0]).to.eq('response') // [1].id).to.match(/setup:dashboard:project-/)
expect(this.send.firstCall.args[1].id).to.match(/open:project-/)
expect(this.send.firstCall.args[1].data).to.eql({ some: 'config' })
})
})
@@ -680,57 +691,57 @@ describe('lib/gui/events', () => {
})
it('sends \'focus:tests\' onFocusTests', function () {
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then(() => {
return this.handleEvent('on:focus:tests')
}).then((assert) => {
this.open.lastCall.args[0].onFocusTests()
const bus = busStub()
return assert.sendCalledWith(undefined)
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e', bus)
.then(() => {
return this.handleEvent('on:focus:tests', '', bus)
}).then(() => {
expect(bus.on).to.have.been.calledWith('focus:tests')
})
})
it('sends \'config:changed\' onSettingsChanged', function () {
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then(() => {
return this.handleEvent('on:config:changed')
}).then((assert) => {
this.open.lastCall.args[0].onSettingsChanged()
const bus = busStub()
return assert.sendCalledWith(undefined)
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e', bus)
.then(() => {
return this.handleEvent('on:config:changed', '', bus)
}).then(() => {
expect(bus.on).to.have.been.calledWith('config:changed')
})
})
it('sends \'spec:changed\' onSpecChanged', function () {
const bus = busStub()
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then(() => {
return this.handleEvent('on:spec:changed')
return this.handleEvent('on:spec:changed', '', bus)
}).then((assert) => {
this.open.lastCall.args[0].onSpecChanged('/path/to/spec.coffee')
return assert.sendCalledWith('/path/to/spec.coffee')
expect(bus.on).to.have.been.calledWith('spec:changed')
})
})
it('sends \'project:warning\' onWarning', function () {
const bus = busStub()
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then(() => {
return this.handleEvent('on:project:warning')
}).then((assert) => {
this.open.lastCall.args[0].onWarning({ name: 'foo', message: 'foo' })
return assert.sendCalledWith({ name: 'foo', message: 'foo' })
return this.handleEvent('on:project:warning', '', bus)
}).then(() => {
expect(bus.on).to.have.been.calledWith('project:warning')
})
})
it('sends \'project:error\' onError', function () {
const bus = busStub()
return this.handleEvent('open:project', '/_test-output/path/to/project-e2e')
.then(() => {
return this.handleEvent('on:project:error')
return this.handleEvent('on:project:error', '', bus)
}).then((assert) => {
this.open.lastCall.args[0].onError({ name: 'foo', message: 'foo' })
return assert.sendCalledWith({ name: 'foo', message: 'foo' })
expect(bus.on).to.have.been.calledWith('project:error')
})
})
@@ -913,18 +924,17 @@ describe('lib/gui/events', () => {
})
describe('setup:dashboard:project', () => {
it('returns result of openProject.createCiProject', function () {
sinon.stub(openProject, 'createCiProject').resolves('response')
it('returns result of ProjectStatic.createCiProject', function () {
return this.handleEvent('setup:dashboard:project').then((assert) => {
return assert.sendCalledWith('response')
expect(this.send.firstCall.args[0]).to.eq('response')
expect(this.send.firstCall.args[1].id).to.match(/setup:dashboard:project-/)
})
})
it('catches errors', function () {
const err = new Error('foo')
sinon.stub(openProject, 'createCiProject').rejects(err)
sinon.stub(ProjectStatic, 'createCiProject').rejects(err)
return this.handleEvent('setup:dashboard:project').then((assert) => {
return assert.sendErrCalledWith(err)
+16 -14
View File
@@ -9,7 +9,6 @@ const pkg = require('@packages/root')
const { fs } = require(`${root}../lib/util/fs`)
const user = require(`${root}../lib/user`)
const errors = require(`${root}../lib/errors`)
const config = require(`${root}../lib/config`)
const ProjectBase = require(`${root}../lib/project-base`).ProjectBase
const browsers = require(`${root}../lib/browsers`)
const Reporter = require(`${root}../lib/reporter`)
@@ -21,6 +20,7 @@ const random = require(`${root}../lib/util/random`)
const system = require(`${root}../lib/util/system`)
const specsUtil = require(`${root}../lib/util/specs`)
const { experimental } = require(`${root}../lib/experiments`)
const ProjectStatic = require(`${root}../lib/project_static`)
describe('lib/modes/run', () => {
beforeEach(function () {
@@ -646,9 +646,21 @@ describe('lib/modes/run', () => {
context('.run browser vs video recording', () => {
beforeEach(function () {
const config = {
proxyUrl: 'http://localhost:12345',
video: true,
videosFolder: 'videos',
integrationFolder: '/path/to/integrationFolder',
resolved: {
integrationFolder: {
integrationFolder: { value: '/path/to/integrationFolder', from: 'config' },
},
},
}
sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync()
sinon.stub(user, 'ensureAuthToken')
sinon.stub(ProjectBase, 'ensureExists').resolves()
sinon.stub(ProjectStatic, 'ensureExists').resolves()
sinon.stub(random, 'id').returns(1234)
sinon.stub(openProject, 'create').resolves(openProject)
sinon.stub(runMode, 'waitForSocketConnection').resolves()
@@ -660,19 +672,9 @@ describe('lib/modes/run', () => {
sinon.spy(runMode, 'waitForBrowserToConnect')
sinon.stub(videoCapture, 'start').resolves()
sinon.stub(openProject, 'launch').resolves()
this.projectInstance.__setConfig(config)
sinon.stub(openProject, 'getProject').resolves(this.projectInstance)
sinon.spy(errors, 'warning')
sinon.stub(config, 'get').resolves({
proxyUrl: 'http://localhost:12345',
video: true,
videosFolder: 'videos',
integrationFolder: '/path/to/integrationFolder',
resolved: {
integrationFolder: {
integrationFolder: { value: '/path/to/integrationFolder', from: 'config' },
},
},
})
sinon.stub(specsUtil, 'find').resolves([
{
@@ -726,7 +728,7 @@ describe('lib/modes/run', () => {
sinon.stub(electron.app, 'on').withArgs('ready').yieldsAsync()
sinon.stub(user, 'ensureAuthToken')
sinon.stub(ProjectBase, 'ensureExists').resolves()
sinon.stub(ProjectStatic, 'ensureExists').resolves()
sinon.stub(random, 'id').returns(1234)
sinon.stub(openProject, 'create').resolves(openProject)
sinon.stub(system, 'info').resolves({ osName: 'osFoo', osVersion: 'fooVersion' })
+15 -2
View File
@@ -1,11 +1,15 @@
require('../spec_helper')
const path = require('path')
const chokidar = require('chokidar')
const browsers = require(`${root}lib/browsers`)
const ProjectBase = require(`${root}lib/project-base`).ProjectBase
const openProject = require(`${root}lib/open_project`)
const preprocessor = require(`${root}lib/plugins/preprocessor`)
const runEvents = require(`${root}lib/plugins/run_events`)
const Fixtures = require('../test/../support/helpers/fixtures')
const todosPath = Fixtures.projectPath('todos')
describe('lib/open_project', () => {
beforeEach(function () {
@@ -23,9 +27,9 @@ describe('lib/open_project', () => {
sinon.stub(browsers, 'get').resolves()
sinon.stub(browsers, 'open')
sinon.stub(ProjectBase.prototype, 'initializeConfig').resolves()
sinon.stub(ProjectBase.prototype, 'open').resolves()
sinon.stub(ProjectBase.prototype, 'reset').resolves()
sinon.stub(ProjectBase.prototype, 'getSpecUrl').resolves()
sinon.stub(ProjectBase.prototype, 'getConfig').resolves(this.config)
sinon.stub(ProjectBase.prototype, 'getAutomation').returns(this.automation)
sinon.stub(preprocessor, 'removeFile')
@@ -34,7 +38,16 @@ describe('lib/open_project', () => {
})
context('#launch', () => {
beforeEach(function () {
beforeEach(async function () {
await openProject.create('/root')
openProject.getProject().__setConfig({
browserUrl: 'http://localhost:8888/__/',
componentFolder: path.join(todosPath, 'component'),
integrationFolder: path.join(todosPath, 'tests'),
projectRoot: todosPath,
specType: 'integration',
})
openProject.getProject().options = {}
this.spec = {
+188 -245
View File
@@ -6,6 +6,7 @@ const commitInfo = require('@cypress/commit-info')
const chokidar = require('chokidar')
const pkg = require('@packages/root')
const Fixtures = require('../support/helpers/fixtures')
const { sinon } = require('../spec_helper')
const api = require(`${root}lib/api`)
const user = require(`${root}lib/user`)
const cache = require(`${root}lib/cache`)
@@ -13,9 +14,21 @@ const config = require(`${root}lib/config`)
const scaffold = require(`${root}lib/scaffold`)
const { ServerE2E } = require(`${root}lib/server-e2e`)
const ProjectBase = require(`${root}lib/project-base`).ProjectBase
const {
getOrgs,
paths,
remove,
add,
getId,
getPathsAndIds,
getProjectStatus,
getProjectStatuses,
createCiProject,
writeProjectId,
} = require(`${root}lib/project_static`)
const ProjectUtils = require(`${root}lib/project_utils`)
const { Automation } = require(`${root}lib/automation`)
const savedState = require(`${root}lib/saved_state`)
const preprocessor = require(`${root}lib/plugins/preprocessor`)
const plugins = require(`${root}lib/plugins`)
const runEvents = require(`${root}lib/plugins/run_events`)
const system = require(`${root}lib/util/system`)
@@ -62,7 +75,7 @@ describe('lib/project-base', () => {
})
it('requires a projectRoot', function () {
const fn = () => new ProjectBase()
const fn = () => new ProjectBase({})
expect(fn).to.throw('Instantiating lib/project requires a projectRoot!')
})
@@ -74,17 +87,19 @@ describe('lib/project-base', () => {
expect(p.projectRoot).to.eq(path.resolve('../foo/bar'))
})
it('handles CT specific behaviors', function () {
it('handles CT specific behaviors', async function () {
sinon.stub(ServerE2E.prototype, 'open').resolves([])
sinon.stub(ProjectBase.prototype, 'startCtDevServer').resolves({ port: 9999 })
const projectCt = new ProjectBase({ projectRoot: '../foo/bar', projectType: 'ct' })
await projectCt.initializeConfig()
return projectCt.open({}).then((project) => {
expect(project._cfg.viewportHeight).to.eq(500)
expect(project._cfg.viewportWidth).to.eq(500)
expect(project._cfg.baseUrl).to.eq('http://localhost:9999')
expect(project.startCtDevServer).to.have.beenCalled
expect(projectCt._cfg.viewportHeight).to.eq(500)
expect(projectCt._cfg.viewportWidth).to.eq(500)
expect(projectCt._cfg.baseUrl).to.eq('http://localhost:9999')
expect(projectCt.startCtDevServer).to.have.beenCalled
})
})
@@ -130,60 +145,57 @@ describe('lib/project-base', () => {
})
})
context('#getConfig', () => {
context('#initializeConfig', () => {
const integrationFolder = 'foo/bar/baz'
beforeEach(function () {
this.project._cfg = undefined
sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar' }).resolves({ baz: 'quux', integrationFolder })
sinon.stub(config, 'get').withArgs(this.todosPath, { foo: 'bar' }).resolves({ baz: 'quux', integrationFolder, browsers: [] })
})
it('calls config.get with projectRoot + options + saved state', function () {
this.project.__setOptions({ foo: 'bar' })
return savedState.create(this.todosPath)
.then((state) => {
.then(async (state) => {
sinon.stub(state, 'get').resolves({ reporterWidth: 225 })
return this.project.getConfig({ foo: 'bar' })
.then((cfg) => {
expect(cfg).to.deep.eq({
integrationFolder,
isNewProject: false,
baz: 'quux',
state: {
reporterWidth: 225,
},
})
this.project._cfg = cfg
await this.project.initializeConfig()
expect(await this.project.getConfig()).to.deep.eq({
integrationFolder,
browsers: [],
isNewProject: false,
baz: 'quux',
state: {
reporterWidth: 225,
},
})
})
})
it('resolves if cfg is already set', function () {
it('resolves if cfg is already set', async function () {
this.project._cfg = {
integrationFolder,
foo: 'bar',
}
return this.project.getConfig()
.then((cfg) => {
expect(cfg).to.deep.eq({
integrationFolder,
foo: 'bar',
})
expect(await this.project.getConfig()).to.deep.eq({
integrationFolder,
foo: 'bar',
})
})
it('sets cfg.isNewProject to false when state.showedNewProjectBanner is true', function () {
this.project.__setOptions({ foo: 'bar' })
return savedState.create(this.todosPath)
.then((state) => {
sinon.stub(state, 'get').resolves({ showedNewProjectBanner: true })
return this.project.getConfig({ foo: 'bar' })
return this.project.initializeConfig()
.then((cfg) => {
expect(cfg).to.deep.eq({
integrationFolder,
browsers: [],
isNewProject: false,
baz: 'quux',
state: {
@@ -197,23 +209,61 @@ describe('lib/project-base', () => {
})
it('does not set cfg.isNewProject when cfg.isTextTerminal', function () {
const cfg = { isTextTerminal: true }
const cfg = { isTextTerminal: true, browsers: [] }
config.get.resolves(cfg)
sinon.stub(this.project, '_setSavedState').resolves(cfg)
return this.project.getConfig({ foo: 'bar' })
return this.project.initializeConfig()
.then((cfg) => {
expect(cfg).not.to.have.property('isNewProject')
})
})
it('attaches warning to non-chrome browsers when chromeWebSecurity:false', async function () {
const cfg = Object.assign({}, {
integrationFolder,
browsers: [{ family: 'chromium', name: 'Canary' }, { family: 'some-other-family', name: 'some-other-name' }],
chromeWebSecurity: false,
})
config.get.restore()
sinon.stub(config, 'get').returns(cfg)
await this.project.initializeConfig()
.then(() => this.project.getConfig())
.then((config) => {
expect(config.chromeWebSecurity).eq(false)
expect(config.browsers).deep.eq([
{
family: 'chromium',
name: 'Canary',
},
{
family: 'some-other-family',
name: 'some-other-name',
warning: `\
Your project has set the configuration option: \`chromeWebSecurity: false\`
This option will not have an effect in Some-other-name. Tests that rely on web security being disabled will not run as expected.\
`,
},
])
expect(config).ok
})
})
})
context('#initializeConfig', function () {
})
context('#open', () => {
beforeEach(function () {
sinon.stub(this.project, 'watchSettingsAndStartWebsockets').resolves()
sinon.stub(this.project, 'checkSupportFile').resolves()
sinon.stub(this.project, 'watchSettings')
sinon.stub(this.project, 'startWebsockets')
this.checkSupportFileStub = sinon.stub(ProjectUtils, 'checkSupportFile').resolves()
sinon.stub(this.project, 'scaffold').resolves()
sinon.stub(this.project, 'getConfig').resolves(this.config)
sinon.stub(ServerE2E.prototype, 'open').resolves([])
@@ -223,44 +273,63 @@ describe('lib/project-base', () => {
sinon.stub(plugins, 'init').resolves()
})
it('calls #watchSettingsAndStartWebsockets with options + config', function () {
const opts = { changeEvents: false, onAutomationRequest () {} }
it('calls #watchSettings with options + config', function () {
return this.project.open().then(() => {
expect(this.project.watchSettings).to.be.calledWith({
configFile: undefined,
onSettingsChanged: false,
projectRoot: this.todosPath,
})
})
})
this.project.cfg = {}
it('calls #startWebsockets with options + config', function () {
const onFocusTests = sinon.stub()
return this.project.open(opts).then(() => {
expect(this.project.watchSettingsAndStartWebsockets).to.be.calledWith(opts, this.project.cfg)
this.project.__setOptions({
onFocusTests,
})
return this.project.open().then(() => {
expect(this.project.startWebsockets).to.be.calledWith({
onReloadBrowser: undefined,
onFocusTests,
onSpecChanged: undefined,
}, {
socketIoCookie: '__socket.io',
namespace: '__cypress',
screenshotsFolder: '/foo/bar/cypress/screenshots',
report: undefined,
reporter: 'spec',
reporterOptions: null,
projectRoot: this.todosPath,
})
})
})
it('calls #scaffold with server config promise', function () {
return this.project.open({}).then(() => {
return this.project.open().then(() => {
expect(this.project.scaffold).to.be.calledWith(this.config)
})
})
it('calls #checkSupportFile with server config when scaffolding is finished', function () {
return this.project.open({}).then(() => {
expect(this.project.checkSupportFile).to.be.calledWith(this.config)
})
})
it('calls #getConfig options', function () {
const opts = {}
return this.project.open(opts).then(() => {
expect(this.project.getConfig).to.be.calledWith(opts)
it('calls checkSupportFile with server config when scaffolding is finished', function () {
return this.project.open().then(() => {
expect(this.checkSupportFileStub).to.be.calledWith({
configFile: 'cypress.json',
supportFile: '/foo/bar/cypress/support/index.js',
})
})
})
it('initializes the plugins', function () {
return this.project.open({}).then(() => {
return this.project.open().then(() => {
expect(plugins.init).to.be.called
})
})
it('calls support.plugins with pluginsFile directory', function () {
return this.project.open({}).then(() => {
return this.project.open().then(() => {
expect(scaffold.plugins).to.be.calledWith(path.dirname(this.config.pluginsFile))
})
})
@@ -272,7 +341,9 @@ describe('lib/project-base', () => {
message: 'plugin error message',
}
return this.project.open({ onError }).then(() => {
this.project.__setOptions({ onError })
return this.project.open().then(() => {
const pluginsOnError = plugins.init.lastCall.args[1].onError
expect(pluginsOnError).to.be.a('function')
@@ -302,36 +373,6 @@ describe('lib/project-base', () => {
})
})
it('attaches warning to non-chrome browsers when chromeWebSecurity:false', function () {
Object.assign(this.config, {
browsers: [{ family: 'chromium', name: 'Canary' }, { family: 'some-other-family', name: 'some-other-name' }],
chromeWebSecurity: false,
})
return this.project.open({})
.then(() => this.project.getConfig())
.then((config) => {
expect(config.chromeWebSecurity).eq(false)
expect(config.browsers).deep.eq([
{
family: 'chromium',
name: 'Canary',
},
{
family: 'some-other-family',
name: 'some-other-name',
warning: `\
Your project has set the configuration option: \`chromeWebSecurity: false\`
This option will not have an effect in Some-other-name. Tests that rely on web security being disabled will not run as expected.\
`,
},
])
expect(config).ok
})
})
it('executes before:run if in interactive mode', function () {
const sysInfo = {
osName: 'darwin',
@@ -342,7 +383,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.config.experimentalInteractiveRunEvents = true
this.config.isTextTerminal = false
return this.project.open({})
return this.project.open()
.then(() => {
expect(runEvents.execute).to.be.calledWith('before:run', this.config, {
config: this.config,
@@ -357,7 +398,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.config.experimentalInteractiveRunEvents = true
this.config.isTextTerminal = true
return this.project.open({})
return this.project.open()
.then(() => {
expect(system.info).not.to.be.called
expect(runEvents.execute).not.to.be.calledWith('before:run')
@@ -369,7 +410,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.config.experimentalInteractiveRunEvents = false
this.config.isTextTerminal = false
return this.project.open({})
return this.project.open()
.then(() => {
expect(system.info).not.to.be.called
expect(runEvents.execute).not.to.be.calledWith('before:run')
@@ -383,7 +424,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('sets firstOpened and lastOpened on first open', function () {
return this.project.open({})
return this.project.open()
.then(() => this.project.getConfig())
.then((config) => {
expect(config.state).to.eql({ firstOpened: this._time, lastOpened: this._time })
@@ -391,11 +432,11 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('only sets lastOpened on subsequent opens', function () {
return this.project.open({})
return this.project.open()
.then(() => {
this._dateStub.returns(this._time + 100000)
})
.then(() => this.project.open({}))
.then(() => this.project.open())
.then(() => this.project.getConfig())
.then((config) => {
expect(config.state).to.eql({ firstOpened: this._time, lastOpened: this._time + 100000 })
@@ -405,9 +446,11 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('updates config.state when saved state changes', function () {
sinon.spy(this.project, 'saveState')
const options = {}
const options = { onSavedStateChanged: (...args) => this.project.saveState(...args) }
return this.project.open(options)
this.project.__setOptions(options)
return this.project.open()
.then(() => options.onSavedStateChanged({ autoScrollingEnabled: false }))
.then(() => this.project.getConfig())
.then((config) => {
@@ -492,12 +535,10 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('resets server + automation', function () {
return this.project.reset()
.then(() => {
expect(this.project._automation.reset).to.be.calledOnce
this.project.reset()
expect(this.project._automation.reset).to.be.calledOnce
expect(this.project.server.reset).to.be.calledOnce
})
expect(this.project.server.reset).to.be.calledOnce
})
})
@@ -614,7 +655,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('watches cypress.json and cypress.env.json', function () {
this.project.watchSettingsAndStartWebsockets({ onSettingsChanged () {} })
this.project.watchSettings({ onSettingsChanged () {} }, {})
expect(this.watch).to.be.calledTwice
expect(this.watch).to.be.calledWith('/path/to/cypress.json')
@@ -622,7 +663,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('sets onChange event when {changeEvents: true}', function (done) {
this.project.watchSettingsAndStartWebsockets({ onSettingsChanged: () => done() })
this.project.watchSettings({ onSettingsChanged: () => done() }, {})
// get the object passed to watchers.watch
const obj = this.watch.getCall(0).args[1]
@@ -633,7 +674,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('does not call watch when {changeEvents: false}', function () {
this.project.watchSettingsAndStartWebsockets({ onSettingsChanged: undefined })
this.project.watchSettings({ onSettingsChanged: undefined }, {})
expect(this.watch).not.to.be.called
})
@@ -645,7 +686,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
const stub = sinon.stub()
this.project.watchSettingsAndStartWebsockets({ onSettingsChanged: stub })
this.project.watchSettings({ onSettingsChanged: stub }, {})
// get the object passed to watchers.watch
const obj = this.watch.getCall(0).args[1]
@@ -663,35 +704,6 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
})
context('#checkSupportFile', () => {
beforeEach(function () {
sinon.stub(fs, 'pathExists').resolves(true)
this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', projectType: 'e2e' })
this.project.server = { onTestFileChange: sinon.spy() }
sinon.stub(preprocessor, 'getFile').resolves()
this.config = {
projectRoot: '/path/to/root/',
supportFile: '/path/to/root/foo/bar.js',
}
})
it('does nothing when {supportFile: false}', function () {
const ret = this.project.checkSupportFile({ supportFile: false })
expect(ret).to.be.undefined
})
it('throws when support file does not exist', function () {
fs.pathExists.resolves(false)
return this.project.checkSupportFile(this.config)
.catch((e) => {
expect(e.message).to.include('The support file is missing or invalid.')
})
})
})
context('#watchPluginsFile', () => {
beforeEach(function () {
sinon.stub(fs, 'pathExists').resolves(true)
@@ -765,18 +777,20 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
})
context('#watchSettingsAndStartWebsockets', () => {
context('#startWebsockets', () => {
beforeEach(function () {
this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', projectType: 'e2e' })
this.project.watchers = {}
this.project._server = { close () {}, startWebsockets: sinon.stub() }
sinon.stub(ProjectBase.prototype, 'open').resolves()
sinon.stub(this.project, 'watchSettings')
})
it('calls server.startWebsockets with automation + config', function () {
it('calls server.startWebsockets with automation + config', async function () {
const c = {}
this.project.watchSettingsAndStartWebsockets({}, c)
this.project.__setConfig(c)
this.project.startWebsockets({}, c)
const args = this.project.server.startWebsockets.lastCall.args
@@ -789,7 +803,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.project.server.startWebsockets.yieldsTo('onReloadBrowser')
this.project.watchSettingsAndStartWebsockets({ onReloadBrowser: fn }, {})
this.project.startWebsockets({ onReloadBrowser: fn }, {})
expect(fn).to.be.calledOnce
})
@@ -869,99 +883,26 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('calls Settings.write with projectRoot and attrs', function () {
return this.project.writeProjectId('id-123').then((id) => {
return writeProjectId('id-123').then((id) => {
expect(id).to.eq('id-123')
})
})
it('sets generatedProjectIdTimestamp', function () {
return this.project.writeProjectId('id-123').then(() => {
// TODO: This
xit('sets generatedProjectIdTimestamp', function () {
return writeProjectId('id-123').then(() => {
expect(this.project.generatedProjectIdTimestamp).to.be.a('date')
})
})
})
context('#getSpecUrl', () => {
beforeEach(function () {
this.project2 = new ProjectBase({ projectRoot: this.idsPath, projectType: 'e2e' })
this.project._cfg = {
browserUrl: 'http://localhost:8888/__/',
integrationFolder: path.join(this.todosPath, 'tests'),
componentFolder: path.join(this.todosPath, 'tests'),
projectRoot: this.todosPath,
}
return settings.write(this.idsPath, { port: 2020 })
})
it('returns fully qualified url when spec exists', function () {
return this.project2.getSpecUrl('cypress/integration/bar.js')
.then((str) => {
expect(str).to.eq('http://localhost:2020/__/#/tests/integration/bar.js')
})
})
it('returns fully qualified url on absolute path to spec', function () {
const todosSpec = path.join(this.todosPath, 'tests/sub/sub_test.coffee')
return this.project.getSpecUrl(todosSpec)
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/sub_test.coffee')
})
})
it('escapses %, &', function () {
const todosSpec = path.join(this.todosPath, 'tests/sub/a&b%c.js')
return this.project.getSpecUrl(todosSpec)
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/a%26b%25c.js')
})
})
// ? is invalid in Windows, but it can be tested here
// because it's a unit test and doesn't check the existence of files
it('escapes ?', function () {
const todosSpec = path.join(this.todosPath, 'tests/sub/a?.spec.js')
return this.project.getSpecUrl(todosSpec)
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/a%3F.spec.js')
})
})
it('escapes %, &, ? in the url dir', function () {
const todosSpec = path.join(this.todosPath, 'tests/s%&?ub/a.spec.js')
return this.project.getSpecUrl(todosSpec)
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/s%25%26%3Fub/a.spec.js')
})
})
it('returns __all spec url', function () {
return this.project.getSpecUrl()
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/__all')
})
})
it('returns __all spec url with spec is __all', function () {
return this.project.getSpecUrl('__all')
.then((str) => {
expect(str).to.eq('http://localhost:8888/__/#/tests/__all')
})
})
})
context('.add', () => {
beforeEach(function () {
this.pristinePath = Fixtures.projectPath('pristine')
})
it('inserts path into cache', function () {
return ProjectBase.add(this.pristinePath, {})
return add(this.pristinePath, {})
.then(() => cache.read()).then((json) => {
expect(json.PROJECTS).to.deep.eq([this.pristinePath])
})
@@ -971,7 +912,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns object containing path and id', function () {
sinon.stub(settings, 'read').resolves({ projectId: 'id-123' })
return ProjectBase.add(this.pristinePath, {})
return add(this.pristinePath, {})
.then((project) => {
expect(project.id).to.equal('id-123')
@@ -984,7 +925,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns object containing just the path', function () {
sinon.stub(settings, 'read').rejects()
return ProjectBase.add(this.pristinePath, {})
return add(this.pristinePath, {})
.then((project) => {
expect(project.id).to.be.undefined
@@ -995,7 +936,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
describe('if configFile is non-default', () => {
it('doesn\'t cache anything and returns object containing just the path', function () {
return ProjectBase.add(this.pristinePath, { configFile: false })
return add(this.pristinePath, { configFile: false })
.then((project) => {
expect(project.id).to.be.undefined
expect(project.path).to.equal(this.pristinePath)
@@ -1009,12 +950,14 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
context('#createCiProject', () => {
const projectRoot = '/_test-output/path/to/project-e2e'
beforeEach(function () {
this.project = new ProjectBase({ projectRoot: '/_test-output/path/to/project-e2e', projectType: 'e2e' })
this.project = new ProjectBase({ projectRoot, projectType: 'e2e' })
this.newProject = { id: 'project-id-123' }
sinon.stub(this.project, 'writeProjectId').resolves('project-id-123')
sinon.stub(user, 'ensureAuthToken').resolves('auth-token-123')
sinon.stub(settings, 'write').resolves('project-id-123')
sinon.stub(commitInfo, 'getRemoteOrigin').resolves('remoteOrigin')
sinon.stub(api, 'createProject')
.withArgs({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')
@@ -1022,19 +965,19 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('calls api.createProject with user session', function () {
return this.project.createCiProject({ foo: 'bar' }).then(() => {
return createCiProject({ foo: 'bar' }, projectRoot).then(() => {
expect(api.createProject).to.be.calledWith({ foo: 'bar' }, 'remoteOrigin', 'auth-token-123')
})
})
it('calls writeProjectId with id', function () {
return this.project.createCiProject({ foo: 'bar' }).then(() => {
expect(this.project.writeProjectId).to.be.calledWith('project-id-123')
return createCiProject({ foo: 'bar' }, projectRoot).then(() => {
expect(settings.write).to.be.calledWith(projectRoot, { projectId: 'project-id-123' })
})
})
it('returns project id', function () {
return this.project.createCiProject({ foo: 'bar' }).then((projectId) => {
return createCiProject({ foo: 'bar' }, projectRoot).then((projectId) => {
expect(projectId).to.eql(this.newProject)
})
})
@@ -1088,15 +1031,15 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('calls cache.removeProject with path', () => {
return ProjectBase.remove('/_test-output/path/to/project-e2e').then(() => {
return remove('/_test-output/path/to/project-e2e').then(() => {
expect(cache.removeProject).to.be.calledWith('/_test-output/path/to/project-e2e')
})
})
})
context('.id', () => {
context('.getId', () => {
it('returns project id', function () {
return ProjectBase.id(this.todosPath).then((id) => {
return getId(this.todosPath).then((id) => {
expect(id).to.eq(this.projectId)
})
})
@@ -1109,7 +1052,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('calls api.getOrgs', () => {
return ProjectBase.getOrgs().then((orgs) => {
return getOrgs().then((orgs) => {
expect(orgs).to.deep.eq([])
expect(api.getOrgs).to.be.calledOnce
expect(api.getOrgs).to.be.calledWith('auth-token-123')
@@ -1123,7 +1066,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('calls cache.getProjectRoots', () => {
return ProjectBase.paths().then((ret) => {
return paths().then((ret) => {
expect(ret).to.deep.eq([])
expect(cache.getProjectRoots).to.be.calledOnce
@@ -1142,7 +1085,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
})
it('returns array of objects with paths and ids', () => {
return ProjectBase.getPathsAndIds().then((pathsAndIds) => {
return getPathsAndIds().then((pathsAndIds) => {
expect(pathsAndIds).to.eql([
{
path: '/path/to/first',
@@ -1165,7 +1108,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('gets projects from api', () => {
sinon.stub(api, 'getProjects').resolves([])
return ProjectBase.getProjectStatuses([])
return getProjectStatuses([])
.then(() => {
expect(api.getProjects).to.have.been.calledWith('auth-token-123')
})
@@ -1174,7 +1117,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns array of projects', () => {
sinon.stub(api, 'getProjects').resolves([])
return ProjectBase.getProjectStatuses([])
return getProjectStatuses([])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses).to.eql([])
})
@@ -1183,7 +1126,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns same number as client projects, even if there are less api projects', () => {
sinon.stub(api, 'getProjects').resolves([])
return ProjectBase.getProjectStatuses([{}])
return getProjectStatuses([{}])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses.length).to.eql(1)
})
@@ -1192,7 +1135,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns same number as client projects, even if there are more api projects', () => {
sinon.stub(api, 'getProjects').resolves([{}, {}])
return ProjectBase.getProjectStatuses([{}])
return getProjectStatuses([{}])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses.length).to.eql(1)
})
@@ -1203,7 +1146,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
{ id: 'id-123', lastBuildStatus: 'passing' },
])
return ProjectBase.getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses[0]).to.eql({
id: 'id-123',
@@ -1217,7 +1160,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('returns client project when it has no id', () => {
sinon.stub(api, 'getProjects').resolves([])
return ProjectBase.getProjectStatuses([{ path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ path: '/_test-output/path/to/project' }])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses[0]).to.eql({
path: '/_test-output/path/to/project',
@@ -1234,7 +1177,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('marks project as invalid if api 404s', () => {
sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 404 })
return ProjectBase.getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses[0]).to.eql({
id: 'id-123',
@@ -1247,7 +1190,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('marks project as unauthorized if api 403s', () => {
sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 403 })
return ProjectBase.getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses[0]).to.eql({
id: 'id-123',
@@ -1260,7 +1203,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('merges in project details and marks valid if somehow project exists and is authorized', () => {
sinon.stub(api, 'getProject').resolves({ id: 'id-123', lastBuildStatus: 'passing' })
return ProjectBase.getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
.then((projectsWithStatuses) => {
expect(projectsWithStatuses[0]).to.eql({
id: 'id-123',
@@ -1276,7 +1219,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
sinon.stub(api, 'getProject').rejects(error)
return ProjectBase.getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
return getProjectStatuses([{ id: 'id-123', path: '/_test-output/path/to/project' }])
.then(() => {
throw new Error('should have caught error but did not')
}).catch((err) => {
@@ -1299,7 +1242,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('gets project from api', function () {
sinon.stub(api, 'getProject').resolves([])
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then(() => {
expect(api.getProject).to.have.been.calledWith('id-123', 'auth-token-123')
})
@@ -1310,7 +1253,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
lastBuildStatus: 'passing',
})
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then((project) => {
expect(project).to.eql({
id: 'id-123',
@@ -1326,7 +1269,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
this.clientProject.id = undefined
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then((project) => {
expect(project).to.eql({
id: undefined,
@@ -1341,7 +1284,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('marks project as invalid if api 404s', function () {
sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 404 })
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then((project) => {
expect(project).to.eql({
id: 'id-123',
@@ -1354,7 +1297,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
it('marks project as unauthorized if api 403s', function () {
sinon.stub(api, 'getProject').rejects({ name: '', message: '', statusCode: 403 })
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then((project) => {
expect(project).to.eql({
id: 'id-123',
@@ -1369,7 +1312,7 @@ This option will not have an effect in Some-other-name. Tests that rely on web s
sinon.stub(api, 'getProject').rejects(error)
return ProjectBase.getProjectStatus(this.clientProject)
return getProjectStatus(this.clientProject)
.then(() => {
throw new Error('should have caught error but did not')
}).catch((err) => {
@@ -0,0 +1,120 @@
import Chai from 'chai'
import { getSpecUrl, checkSupportFile } from '../../lib/project_utils'
import Fixtures from '../support/helpers/fixtures'
import path from 'path'
const todosPath = Fixtures.projectPath('todos')
const defaultProps = {
browserUrl: 'http://localhost:8888/__/',
componentFolder: path.join(todosPath, 'component'),
integrationFolder: path.join(todosPath, 'tests'),
projectRoot: todosPath,
specType: 'integration',
} as const
const expect = Chai.expect
describe('lib/project_utils', () => {
describe('getSpecUrl', () => {
it('normalizes to __all when absoluteSpecUrl is undefined', () => {
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: undefined,
})
expect(str).to.eq('http://localhost:8888/__/#/tests/__all')
})
it('normalizes to __all when absoluteSpecUrl is __all', () => {
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: '__all',
})
expect(str).to.eq('http://localhost:8888/__/#/tests/__all')
})
it('normalizes to __all when absoluteSpecUrl is __all', () => {
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: '__all',
})
expect(str).to.eq('http://localhost:8888/__/#/tests/__all')
})
it('returns fully qualified url when spec exists', function () {
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: 'cypress/integration/bar.js',
})
expect(str).to.eq('http://localhost:8888/__/#/tests/cypress/integration/bar.js')
})
it('returns fully qualified url on absolute path to spec', function () {
const todosSpec = path.join(todosPath, 'tests/sub/sub_test.coffee')
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: todosSpec,
})
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/sub_test.coffee')
})
it('escapses %, &', function () {
const todosSpec = path.join(todosPath, 'tests/sub/a&b%c.js')
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: todosSpec,
})
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/a%26b%25c.js')
})
// ? is invalid in Windows, but it can be tested here
// because it's a unit test and doesn't check the existence of files
it('escapes ?', function () {
const todosSpec = path.join(todosPath, 'tests/sub/a?.spec.js')
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: todosSpec,
})
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/sub/a%3F.spec.js')
})
it('escapes %, &, ? in the url dir', function () {
const todosSpec = path.join(todosPath, 'tests/s%&?ub/a.spec.js')
const str = getSpecUrl({
...defaultProps,
absoluteSpecPath: todosSpec,
})
expect(str).to.eq('http://localhost:8888/__/#/tests/integration/s%25%26%3Fub/a.spec.js')
})
})
describe('checkSupportFile', () => {
it('does nothing when {supportFile: false}', async () => {
const ret = await checkSupportFile({
configFile: 'cypress.json',
supportFile: false,
})
expect(ret).to.be.undefined
})
it('throws when support file does not exist', async () => {
try {
await checkSupportFile({
configFile: 'cypress.json',
supportFile: '/this/file/does/not/exist/foo/bar/cypress/support/index.js',
})
} catch (e) {
expect(e.message).to.include('The support file is missing or invalid.')
}
})
})
})
+43 -17
View File
@@ -28,8 +28,10 @@ describe('lib/scaffold', () => {
it('is true when integrationFolder is empty', function () {
const pristine = new ProjectBase({ projectRoot: this.pristinePath, projectType: 'e2e' })
return pristine.getConfig()
.then((cfg) => {
return pristine.initializeConfig()
.then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.true
@@ -37,10 +39,18 @@ describe('lib/scaffold', () => {
})
it('is false when integrationFolder has been changed', function () {
const pristine = new ProjectBase({ projectRoot: this.pristinePath, projectType: 'e2e' })
const pristine = new ProjectBase({
projectRoot: this.pristinePath,
projectType: 'e2e',
options: {
integrationFolder: 'foo',
},
})
return pristine.getConfig({ integrationFolder: 'foo' })
.then((cfg) => {
return pristine.initializeConfig()
.then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
expect(ret).to.be.false
@@ -53,8 +63,10 @@ describe('lib/scaffold', () => {
this.ids = new ProjectBase({ projectRoot: idsPath, projectType: 'e2e' })
return this.ids.getConfig()
.then((cfg) => {
return this.ids.initializeConfig()
.then(() => {
return this.ids.getConfig()
}).then((cfg) => {
return this.ids.scaffold(cfg).return(cfg)
}).then((cfg) => {
return this.ids.determineIsNewProject(cfg)
@@ -68,9 +80,13 @@ describe('lib/scaffold', () => {
this.todos = new ProjectBase({ projectRoot: todosPath, projectType: 'e2e' })
return this.todos.getConfig()
.then((cfg) => {
return this.todos.scaffold(cfg).return(cfg)
return this.todos.initializeConfig()
.then(() => {
return this.todos.getConfig()
}).then((cfg) => {
return this.todos.scaffold(cfg)
}).then(() => {
return this.todos.getConfig()
}).then((cfg) => {
return this.todos.determineIsNewProject(cfg)
}).then((ret) => {
@@ -84,9 +100,13 @@ describe('lib/scaffold', () => {
it('is true when files, name + bytes match to scaffold', function () {
const pristine = new ProjectBase({ projectRoot: this.pristinePath, projectType: 'e2e' })
return pristine.getConfig()
.then((cfg) => {
return pristine.scaffold(cfg).return(cfg)
return pristine.initializeConfig()
.then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.scaffold(cfg)
}).then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.determineIsNewProject(cfg)
}).then((ret) => {
@@ -97,9 +117,13 @@ describe('lib/scaffold', () => {
it('is false when bytes dont match scaffold', function () {
const pristine = new ProjectBase({ projectRoot: this.pristinePath, projectType: 'e2e' })
return pristine.getConfig()
.then((cfg) => {
return pristine.scaffold(cfg).return(cfg)
return pristine.initializeConfig()
.then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.scaffold(cfg)
}).then(() => {
return pristine.getConfig()
}).then((cfg) => {
const file = path.join(cfg.integrationFolder, '1-getting-started', 'todo.spec.js')
@@ -109,8 +133,10 @@ describe('lib/scaffold', () => {
.then((str) => {
str += 'foo bar baz'
return fs.writeFileAsync(file, str).return(cfg)
return fs.writeFileAsync(file, str)
})
}).then(() => {
return pristine.getConfig()
}).then((cfg) => {
return pristine.determineIsNewProject(cfg)
}).then((ret) => {