mirror of
https://github.com/cypress-io/cypress.git
synced 2026-04-29 19:41:16 -05:00
feat: improved DX and support for running component and e2e tests w/ gulp (#18135)
Co-authored-by: Tim Griesser <tgriesser10@gmail.com>
This commit is contained in:
@@ -1,63 +1,135 @@
|
||||
/**
|
||||
* How the Cypress backend is started and watched. Formerly
|
||||
* `node scripts/cypress.js open` or `node scripts/cypress.js run`
|
||||
*
|
||||
* @summary Gulp tasks to run the Cypress app.
|
||||
*/
|
||||
|
||||
import chokidar from 'chokidar'
|
||||
import path from 'path'
|
||||
import childProcess, { ChildProcess } from 'child_process'
|
||||
import pDefer from 'p-defer'
|
||||
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { getGulpGlobal } from '../gulpConstants'
|
||||
import { ENV_VARS, getGulpGlobal } from '../gulpConstants'
|
||||
import { forked } from '../utils/childProcessUtils'
|
||||
import { exitAndRemoveProcess } from './gulpRegistry'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
|
||||
/**
|
||||
* Starts cypress, but watches the GraphQL files & restarts the server
|
||||
* when any of those change
|
||||
*/
|
||||
export function startCypressWatch () {
|
||||
const shouldWatch = getGulpGlobal('shouldWatch')
|
||||
const pathToCli = path.resolve(monorepoPaths.root, 'cli', 'bin', 'cypress')
|
||||
|
||||
const watcher = shouldWatch ? chokidar.watch([
|
||||
/**------------------------------------------------------------------------
|
||||
* Cypress CLI
|
||||
* Starts Cypress, like a user would.
|
||||
* * openCypress - Normal `cypress open` command
|
||||
* * runCypress - Normal `cypress run` command
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export async function openCypressLaunchpad () {
|
||||
return spawnCypressWithMode('open', 'dev', ENV_VARS.DEV_OPEN, ['--project', monorepoPaths.pkgLaunchpad])
|
||||
}
|
||||
|
||||
export async function openCypressApp () {
|
||||
return spawnCypressWithMode('open', 'dev', ENV_VARS.DEV_OPEN, ['--project', monorepoPaths.pkgApp])
|
||||
}
|
||||
|
||||
export async function runCypressLaunchpad () {
|
||||
return spawnCypressWithMode('run', 'dev', ENV_VARS.PROD, ['--project', monorepoPaths.pkgLaunchpad])
|
||||
}
|
||||
|
||||
export async function runCypressApp () {
|
||||
return spawnCypressWithMode('run', 'dev', ENV_VARS.PROD, ['--project', monorepoPaths.pkgApp])
|
||||
}
|
||||
|
||||
export async function runCypressProd () {
|
||||
return spawnCypressWithMode('run', 'prod', ENV_VARS.PROD)
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Testing Tasks
|
||||
* Building and running the Cypress app and graphql server for testing.
|
||||
* * startCypressForTest - Start the Cypress server, but without watching
|
||||
* * runCypressAgainstDist - Serve the dist'd frontend over file://
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
// Use the GQL Test Port (52300 by default, defined in ./gulp/gulpConstants)
|
||||
// Spawns Cypress in "Test Cypress within Cypress" mode
|
||||
export async function startCypressForTest () {
|
||||
return spawnCypressWithMode('open', 'test', ENV_VARS.E2E_TEST_TARGET)
|
||||
}
|
||||
|
||||
export async function runCypressAgainstDist () {
|
||||
return spawnCypressWithMode('run', 'test', ENV_VARS.E2E_TEST_TARGET)
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Start and Watch Utils
|
||||
* * spawnCypressWithMode - Formerly known as: `node ./scripts/cypress.js run`
|
||||
* * watchCypress - Watch the dev server and graphql files
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
async function spawnCypressWithMode (
|
||||
mode: 'open' | 'run',
|
||||
type: 'dev' | 'prod' | 'test',
|
||||
env: Record<string, string> = {},
|
||||
additionalArgv: string[] = [],
|
||||
) {
|
||||
let argv = process.argv.slice(3).concat(additionalArgv)
|
||||
|
||||
const debugFlag = getGulpGlobal('debug')
|
||||
|
||||
if (debugFlag) {
|
||||
env = { ...env, CYPRESS_INTERNAL_DEV_DEBUG: debugFlag }
|
||||
}
|
||||
|
||||
if (mode === 'open') {
|
||||
if (!argv.includes('--project') && !argv.includes('--global')) {
|
||||
argv.push('--global')
|
||||
}
|
||||
|
||||
// If we've passed --record, it's for a "run" mode, probably in the same pipeline.
|
||||
if (argv.includes('--record')) {
|
||||
argv = argv.slice(0, argv.indexOf('--record'))
|
||||
}
|
||||
}
|
||||
|
||||
if (!argv.includes('--dev')) {
|
||||
argv.push('--dev')
|
||||
}
|
||||
|
||||
const finalEnv = {
|
||||
...process.env,
|
||||
...env,
|
||||
LAUNCHPAD: '1',
|
||||
}
|
||||
|
||||
return await forked(`cy:${mode}:${type}`, pathToCli, [mode, ...argv], {
|
||||
env: finalEnv,
|
||||
waitForData: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Watch Commands
|
||||
* Starts Cypress, but watches the GraphQL files, and restarts the server.
|
||||
* * startCypressWatch - Normal `cypress open` command, with watching
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export async function startCypressWatch () {
|
||||
const watcher = chokidar.watch([
|
||||
'packages/graphql/src/**/*.{js,ts}',
|
||||
'packages/server/lib/graphql/**/*.{js,ts}',
|
||||
], {
|
||||
cwd: monorepoPaths.root,
|
||||
ignored: /\.gen\.ts/,
|
||||
ignoreInitial: true,
|
||||
}) : null
|
||||
|
||||
let child: ChildProcess | null = null
|
||||
})
|
||||
|
||||
let isClosing = false
|
||||
let isRestarting = false
|
||||
let child: ChildProcess | null = null
|
||||
|
||||
const argv = process.argv.slice(3)
|
||||
const pathToCli = path.resolve(monorepoPaths.root, 'cli', 'bin', 'cypress')
|
||||
|
||||
function openServer () {
|
||||
if (child) {
|
||||
child.removeAllListeners()
|
||||
}
|
||||
|
||||
if (!argv.includes('--project') && !argv.includes('--global')) {
|
||||
argv.push('--global')
|
||||
}
|
||||
|
||||
if (!argv.includes('--dev')) {
|
||||
argv.push('--dev')
|
||||
}
|
||||
|
||||
const debugFlag = getGulpGlobal('debug')
|
||||
|
||||
if (debugFlag) {
|
||||
process.env.CYPRESS_INTERNAL_DEV_DEBUG = debugFlag
|
||||
}
|
||||
|
||||
child = childProcess.fork(pathToCli, ['open', ...argv], {
|
||||
stdio: 'inherit',
|
||||
execArgv: [],
|
||||
env: {
|
||||
...process.env,
|
||||
LAUNCHPAD: '1',
|
||||
CYPRESS_INTERNAL_DEV_WATCH: shouldWatch ? 'true' : undefined,
|
||||
},
|
||||
})
|
||||
async function startCypressWithListeners () {
|
||||
child = await spawnCypressWithMode('open', 'dev', ENV_VARS.DEV)
|
||||
|
||||
child.on('exit', (code) => {
|
||||
if (isClosing) {
|
||||
@@ -78,27 +150,49 @@ export function startCypressWatch () {
|
||||
const dfd = pDefer()
|
||||
|
||||
if (child) {
|
||||
child.on('exit', dfd.resolve)
|
||||
isRestarting = true
|
||||
child.send('close')
|
||||
child.on('exit', dfd.resolve)
|
||||
await exitAndRemoveProcess(child)
|
||||
} else {
|
||||
dfd.resolve()
|
||||
}
|
||||
|
||||
await dfd.promise
|
||||
|
||||
if (child) {
|
||||
child.removeAllListeners()
|
||||
}
|
||||
|
||||
await startCypressWithListeners()
|
||||
isRestarting = false
|
||||
openServer()
|
||||
}
|
||||
|
||||
if (shouldWatch) {
|
||||
watcher?.on('add', restartServer)
|
||||
watcher?.on('change', restartServer)
|
||||
}
|
||||
watcher.on('add', restartServer)
|
||||
watcher.on('change', restartServer)
|
||||
|
||||
openServer()
|
||||
await startCypressWithListeners()
|
||||
|
||||
process.on('beforeExit', () => {
|
||||
isClosing = true
|
||||
child?.send('close')
|
||||
watcher.close()
|
||||
})
|
||||
}
|
||||
|
||||
export function wrapRunWithExit (proc: ChildProcess) {
|
||||
function killAndExit (code: number) {
|
||||
process.exit(code)
|
||||
}
|
||||
|
||||
proc.on('exit', (code) => {
|
||||
killAndExit(code ?? 0)
|
||||
})
|
||||
|
||||
proc.on('error', (err) => {
|
||||
console.error({ err })
|
||||
killAndExit(1)
|
||||
})
|
||||
|
||||
proc.on('disconnect', () => {
|
||||
console.error('disconnected')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { nexusTypegen, watchNexusTypegen } from '../utils/nexusTypegenUtil'
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { spawned } from '../utils/childProcessUtils'
|
||||
import { spawn } from 'child_process'
|
||||
import { CYPRESS_INTERNAL_CLOUD_ENV } from '../gulpConstants'
|
||||
import { DEFAULT_INTERNAL_CLOUD_ENV } from '../gulpConstants'
|
||||
|
||||
export async function nexusCodegen () {
|
||||
return nexusTypegen({
|
||||
@@ -89,12 +89,12 @@ const ENV_MAP = {
|
||||
}
|
||||
|
||||
export async function syncRemoteGraphQL () {
|
||||
if (!ENV_MAP[CYPRESS_INTERNAL_CLOUD_ENV]) {
|
||||
if (!ENV_MAP[DEFAULT_INTERNAL_CLOUD_ENV]) {
|
||||
throw new Error(`Expected --env to be one of ${Object.keys(ENV_MAP).join(', ')}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await rp.get(`${ENV_MAP[CYPRESS_INTERNAL_CLOUD_ENV]}/test-runner-graphql-schema`)
|
||||
const body = await rp.get(`${ENV_MAP[DEFAULT_INTERNAL_CLOUD_ENV]}/test-runner-graphql-schema`)
|
||||
|
||||
// TODO(tim): fix
|
||||
await fs.ensureDir(path.join(monorepoPaths.pkgGraphql, 'src/gen'))
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import pDefer from 'p-defer'
|
||||
import treeKill from 'tree-kill'
|
||||
|
||||
const childProcesses = new Set<ChildProcess>()
|
||||
const exitedPids = new Set<number>()
|
||||
|
||||
let hasExited = false
|
||||
|
||||
export function addChildProcess (child: ChildProcess) {
|
||||
if (hasExited) {
|
||||
treeKill(child.pid)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
childProcesses.add(child)
|
||||
child.on('exit', () => {
|
||||
if (!hasExited) {
|
||||
exitAndRemoveProcess(child)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function exitAndRemoveProcess (child: ChildProcess) {
|
||||
if (exitedPids.has(child.pid)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!childProcesses.has(child)) {
|
||||
throw new Error(`Cannot remove child process ${child.pid}, it was never registered`)
|
||||
}
|
||||
|
||||
childProcesses.delete(child)
|
||||
|
||||
const dfd = pDefer()
|
||||
|
||||
exitedPids.add(child.pid)
|
||||
treeKill(child.pid, (err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
dfd.resolve()
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
export async function exitAllProcesses () {
|
||||
await Promise.all(Array.from(childProcesses).map(exitAndRemoveProcess))
|
||||
}
|
||||
|
||||
process.stdin.resume() //so the program will not close instantly
|
||||
|
||||
export async function exitAfterAll () {
|
||||
process.stdin.pause()
|
||||
}
|
||||
|
||||
function exitHandler (msg: string) {
|
||||
return async function _exitHandler (exitCode: number) {
|
||||
hasExited = true
|
||||
console.log(`Exiting due to ${msg} => code ${exitCode}`)
|
||||
await exitAllProcesses()
|
||||
process.exit(exitCode)
|
||||
}
|
||||
}
|
||||
|
||||
// do something when app is closing
|
||||
process.on('exit', exitHandler('exit'))
|
||||
|
||||
// catches ctrl+c event
|
||||
process.on('SIGINT', exitHandler('SIGINT'))
|
||||
|
||||
// catches "kill pid" (for example: nodemon restart)
|
||||
process.on('SIGUSR1', exitHandler('SIGUSR1'))
|
||||
process.on('SIGUSR2', exitHandler('SIGUSR2'))
|
||||
|
||||
// catches uncaught exceptions
|
||||
process.on('uncaughtException', exitHandler('uncaughtException'))
|
||||
+168
-27
@@ -1,21 +1,104 @@
|
||||
import type { SpawnOptions } from 'child_process'
|
||||
import getenv from 'getenv'
|
||||
import pDefer from 'p-defer'
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { AllSpawnableApps, spawned } from '../utils/childProcessUtils'
|
||||
/**
|
||||
* The Launchpad and App clients are both built with Vite and largely
|
||||
* share the same test pipeline and build commands.
|
||||
*
|
||||
* @summary Build pipeline for the Vite frontend(s):
|
||||
* @packages/launchpad + @packages/app
|
||||
* @docs https://vitejs.dev
|
||||
*/
|
||||
|
||||
const CYPRESS_VITE_APP_PORT = getenv.int('CYPRESS_VITE_APP_PORT', 3333)
|
||||
const CYPRESS_VITE_LAUNCHPAD_PORT = getenv.int('CYPRESS_VITE_LAUNCHPAD_PORT', 3001)
|
||||
import type { SpawnOptions } from 'child_process'
|
||||
import { ENV_VARS } from '../gulpConstants'
|
||||
|
||||
import { monorepoPaths } from '../monorepoPaths'
|
||||
import { AllSpawnableApps, spawned, spawnUntilMatch } from '../utils/childProcessUtils'
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Local Development Workflow
|
||||
* Spawn the Vite frontend dev servers in watch mode.
|
||||
* * viteApp
|
||||
* * viteLaunchpad
|
||||
* * watchViteBuild
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export function viteApp () {
|
||||
return viteDev('vite-app', `vite --port ${CYPRESS_VITE_APP_PORT} --base /__vite__/`, {
|
||||
const GQL_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_GQL_PORT
|
||||
const APP_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_VITE_APP_PORT
|
||||
|
||||
return spawnViteDevServer('vite-app', `yarn vite --port ${APP_PORT} --base /__vite__/`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteLaunchpad () {
|
||||
return viteDev('vite-launchpad', `vite --port ${CYPRESS_VITE_LAUNCHPAD_PORT}`, {
|
||||
const GQL_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_GQL_PORT
|
||||
const LAUNCHPAD_PORT = ENV_VARS.DEV.CYPRESS_INTERNAL_VITE_LAUNCHPAD_PORT
|
||||
|
||||
return spawnViteDevServer('vite-launchpad', `yarn vite --port ${LAUNCHPAD_PORT}`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// This watcher task is generally used within cypress:open when running in
|
||||
// end-to-end mode.
|
||||
function watchViteBuild (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
options: SpawnOptions = {},
|
||||
) {
|
||||
// This will match strings like "built in 200ms" and "built in 5s"
|
||||
return spawnUntilMatch(prefix, {
|
||||
command,
|
||||
match: /built in (\d+)(m?s)/i,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
function spawnViteDevServer (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
options: SpawnOptions = {},
|
||||
) {
|
||||
return spawnUntilMatch(prefix, {
|
||||
command,
|
||||
match: 'dev server running at',
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
/**------------------------------------------------------------------------
|
||||
* Build Tasks
|
||||
* Build the Vite frontend(s) for production to be served by the Launchpad
|
||||
* and App. Generally used in CI.
|
||||
* * viteBuildApp
|
||||
* * viteBuildLaunchpad
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
export function viteBuildApp () {
|
||||
return spawned('vite:build-app', `yarn vite build`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteBuildLaunchpad () {
|
||||
return spawned('vite:build-launchpad', `yarn vite build`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,25 +116,83 @@ export function viteCleanLaunchpad () {
|
||||
})
|
||||
}
|
||||
|
||||
function viteDev (
|
||||
prefix: AllSpawnableApps,
|
||||
command: string,
|
||||
opts: SpawnOptions = {},
|
||||
/**------------------------------------------------------------------------
|
||||
* Testing Tasks
|
||||
* Build and serve the Vite frontend(s) as web apps on a static server.
|
||||
* * viteBuildLaunchpadForTest
|
||||
* * viteBuildAppForTest
|
||||
* * serveBuiltLaunchpadForTest
|
||||
* * serveBuiltAppForTest
|
||||
*------------------------------------------------------------------------**/
|
||||
|
||||
) {
|
||||
const dfd = pDefer()
|
||||
let ready = false
|
||||
|
||||
spawned(prefix, command, opts, {
|
||||
tapOut (chunk, enc, cb) {
|
||||
if (!ready && String(chunk).includes('dev server running at')) {
|
||||
ready = true
|
||||
setTimeout(() => dfd.resolve(), 20) // flush the rest of the chunks
|
||||
}
|
||||
|
||||
cb(null, chunk)
|
||||
// After running `serveBuiltLaunchpadForTest`, you're able to visit
|
||||
// `http://localhost:5555` to access the Launchpad frontend.
|
||||
export function serveBuiltLaunchpadForTest () {
|
||||
return spawnUntilMatch('serve:launchpad-for-test', {
|
||||
command: `yarn serve ./dist-e2e -p 5555`,
|
||||
match: 'Accepting connections',
|
||||
options: {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function viteBuildLaunchpadForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return spawned('vite:build-launchpad-for-test', `yarn vite build --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function viteBuildAndWatchLaunchpadForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return watchViteBuild('vite:build-watch-launchpad-for-test', `yarn vite build --watch --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgLaunchpad,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**----------------------
|
||||
*todo Implement E2E tests for the App.
|
||||
*------------------------**/
|
||||
|
||||
export function viteBuildAppForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return spawned('vite:build-app-for-test', `yarn vite build --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
waitForExit: true,
|
||||
env: {
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
...process.env,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function serveBuiltAppForTest () {
|
||||
return spawned('serve:app-for-test', `yarn serve ./dist-e2e -p 5556`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
})
|
||||
}
|
||||
|
||||
export async function viteBuildAndWatchAppForTest () {
|
||||
const GQL_PORT = ENV_VARS.E2E_TEST_TARGET.CYPRESS_INTERNAL_GQL_PORT
|
||||
|
||||
return watchViteBuild('vite:build-watch-app-for-test', `yarn vite build --watch --outDir=./dist-e2e`, {
|
||||
cwd: monorepoPaths.pkgApp,
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
VITE_CYPRESS_INTERNAL_GQL_PORT: GQL_PORT,
|
||||
},
|
||||
})
|
||||
|
||||
return dfd.promise
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user