Files
cypress/scripts/binary/smoke.js
Bill Glesias 3481d1acaf chore: refactor cypress/cli to TypeScript (#32063)
* migrate cli scripts to TypeScript

* convert all javascript source files in the CLI to TypeScript

rebase into first

* chore: refactor all tests to TypeScript

rebase into second

* add npmignore for cli for typescript files

* update build process

* fix publically available exports

* Fix cy-in-cy tests

* add ts-expect-error to failing files

* fix projectConfigIpc failures as there are now multiple installs of tsx

* fix after-pack hook

* fix binary script

* chore: update publish binary to account for CLI being an ESModule compiled down to CommonJS

* does this work?

* fix the verify spec by making the listr2 renderer silent as it behaves differently since the refactor and is printing non deterministic outputs into our tests that do not have a large impact on the area we are testing and mostly served to actually test the renders of the listr2 framework itself

* empty commit

* additional refactor to code to remove strange any typing and exporting

* bump cache and build binaries

* fix CLI exports to keep backwards compatibility

* fix unit-tests

* turn on mac jobs

* fix group name rename in CLI

* remove babel deps from cli and explicitly install typescript

* address feedback from code review

* dont just falsy check results and instead explicitly check for null or undefined

* add ts-expect-error

* additional pass on cleaning up dynamic require / import from global lib references

* annotate ts-expect-errors with reason for why error is expected

* add rest of ts-expect-error comments

* removing hardcoded branch to publish binary chore/migrate_cli_to_typescript
2025-09-02 17:52:45 -04:00

351 lines
10 KiB
JavaScript

const _ = require('lodash')
const fse = require('fs-extra')
const cp = require('child_process')
const execa = require('execa')
const path = require('path')
const Promise = require('bluebird')
const os = require('os')
const verify = require('../../cli/lib/tasks/verify').default
const Fixtures = require('@tooling/system-tests')
const { scaffoldCommonNodeModules } = require('@tooling/system-tests/lib/dep-installer')
const fs = Promise.promisifyAll(fse)
const canRecordVideo = () => {
return os.platform() !== 'win32'
}
const shouldSkipProjectTest = () => {
return os.platform() === 'win32'
}
const runSmokeTest = function (buildAppExecutable, timeoutSeconds = 30) {
const rand = String(_.random(0, 1000))
console.log(`executable path ${buildAppExecutable}`)
console.log(`timeout ${timeoutSeconds} seconds`)
const hasRightResponse = function (stdout) {
// there could be more debug lines in the output, so find 1 line with
// expected random value
const lines = stdout.split('\n').map((s) => {
return s.trim()
})
return lines.includes(rand)
}
const args = []
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
// separate any Electron command line arguments from Cypress args
args.push('--')
args.push('--smoke-test')
args.push(`--ping=${rand}`)
const options = {
timeout: timeoutSeconds * 1000,
}
return execa(`${buildAppExecutable}`, args, options)
.catch((err) => {
console.error('smoke test failed with error %s', err.message)
throw err
}).then(({ stdout }) => {
stdout = stdout.replace(/\s/, '')
if (!hasRightResponse(stdout)) {
throw new Error(`Stdout: '${stdout}' did not match the random number: '${rand}'`)
}
console.log('smoke test response', stdout)
return console.log('smokeTest passes')
})
}
const runProjectTest = function (buildAppExecutable, e2e) {
if (shouldSkipProjectTest()) {
console.log('skipping project test')
return Promise.resolve()
}
return new Promise((resolve, reject) => {
const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV')
if (!canRecordVideo()) {
console.log('cannot record video on this platform yet, disabling')
env.CYPRESS_VIDEO_RECORDING = 'false'
}
const args = [
`--run-project=${e2e}`,
`--spec=${e2e}/cypress/e2e/simple_passing.cy.js`,
]
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const options = {
stdio: 'inherit', env,
}
console.log('running project test')
console.log(buildAppExecutable, args.join(' '))
return cp.spawn(buildAppExecutable, args, options)
.on('exit', (code) => {
if (code === 0) {
return resolve()
}
return reject(new Error(`running project tests failed with: '${code}' errors.`))
})
})
}
const runFailingProjectTest = function (buildAppExecutable, e2e) {
if (shouldSkipProjectTest()) {
console.log('skipping failing project test')
return Promise.resolve()
}
console.log('running failing project test')
const verifyScreenshots = function () {
const screenshot1 = path.join(e2e, 'cypress', 'screenshots', 'simple_failing.cy.js', 'simple failing spec -- fails1 (failed).png')
const screenshot2 = path.join(e2e, 'cypress', 'screenshots', 'simple_failing.cy.js', 'simple failing spec -- fails2 (failed).png')
return Promise.all([
fs.statAsync(screenshot1),
fs.statAsync(screenshot2),
])
}
const spawn = () => {
return new Promise((resolve, reject) => {
const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV')
const args = [
`--run-project=${e2e}`,
`--spec=${e2e}/cypress/e2e/simple_failing.cy.js`,
]
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const options = {
stdio: 'inherit',
env,
}
return cp.spawn(buildAppExecutable, args, options)
.on('exit', (code) => {
if (code === 2) {
return resolve()
}
return reject(new Error(`running project tests failed with: '${code}' errors.`))
})
})
}
return spawn()
.then(verifyScreenshots)
}
const runV8SnapshotProjectTest = function (buildAppExecutable, e2e) {
if (shouldSkipProjectTest()) {
console.log('skipping failing project test')
return Promise.resolve()
}
console.log('running v8 snapshot project test')
const spawn = () => {
return new Promise((resolve, reject) => {
const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV')
const args = [
`--run-project=${e2e}`,
`--spec=${e2e}/cypress/e2e/simple_v8_snapshot.cy.js`,
]
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const options = {
stdio: 'inherit',
env,
}
return cp.spawn(buildAppExecutable, args, options)
.on('exit', (code) => {
if (code === 0) {
return resolve()
}
return reject(new Error(`running project tests failed with: '${code}' errors.`))
})
})
}
return spawn()
}
const runErroringProjectTest = function (buildAppExecutable, e2e, testName, errorMessage) {
return new Promise((resolve, reject) => {
const env = _.omit(process.env, 'CYPRESS_INTERNAL_ENV')
if (!canRecordVideo()) {
console.log('cannot record video on this platform yet, disabling')
env.CYPRESS_VIDEO_RECORDING = 'false'
}
const args = [
`--run-project=${e2e}`,
`--spec=${e2e}/cypress/e2e/simple_passing.cy.js`,
]
if (verify.needsSandbox()) {
args.push('--no-sandbox')
}
const options = {
stdio: ['inherit', 'inherit', 'pipe'], env,
}
console.log('running project test')
console.log(buildAppExecutable, args.join(' '))
const childProcess = cp.spawn(buildAppExecutable, args, options)
let errorOutput = ''
childProcess.stderr.on('data', (data) => {
errorOutput += data.toString()
})
childProcess.on('exit', (code) => {
if (code === 0) {
return reject(new Error(`running project tests should have failed for test: ${testName}`))
}
if (!errorOutput.includes(errorMessage)) {
return reject(new Error(`running project tests failed with errors: ${errorOutput} but did not include the expected error message: '${errorMessage}'`))
}
return resolve()
})
})
}
const runIntegrityTest = async function (buildAppExecutable, buildAppDir, e2e) {
const testCorruptingFile = async (file, errorMessage) => {
const contents = await fs.readFile(file)
// Backup state
await fs.move(file, `${file}.bak`)
// Modify app
await fs.writeFile(file, Buffer.concat([contents, Buffer.from(`\nconsole.log('modified code')`)]))
await runErroringProjectTest(buildAppExecutable, e2e, `corrupting ${file}`, errorMessage)
// Restore original state
await fs.move(`${file}.bak`, file, { overwrite: true })
}
await testCorruptingFile(path.join(buildAppDir, 'index.js'), 'Integrity check failed for main index.js file')
await testCorruptingFile(path.join(buildAppDir, 'packages', 'server', 'index.jsc'), 'Integrity check failed for main server index.jsc file')
const testAlteringEntryPoint = async (additionalCode, errorMessage) => {
const packageJsonContents = await fs.readJSON(path.join(buildAppDir, 'package.json'))
// Backup state
await fs.move(path.join(buildAppDir, 'package.json'), path.join(buildAppDir, 'package.json.bak'))
// Modify app
await fs.writeJSON(path.join(buildAppDir, 'package.json'), {
...packageJsonContents,
main: 'index2.js',
})
await fs.writeFile(path.join(buildAppDir, 'index2.js'), `${additionalCode}\nrequire("./index.js")`)
await runErroringProjectTest(buildAppExecutable, e2e, 'altering entry point', errorMessage)
// Restore original state
await fs.move(path.join(buildAppDir, 'package.json.bak'), path.join(buildAppDir, 'package.json'), { overwrite: true })
await fs.remove(path.join(buildAppDir, 'index2.js'))
}
await testAlteringEntryPoint('console.log("simple alteration")', 'Integrity check failed with expected stack length 10 but got 12')
await testAlteringEntryPoint('console.log("accessing " + global.getSnapshotResult())', 'getSnapshotResult can only be called once')
function compareGlobals () {
const childProcess = require('child_process')
const nodeGlobalKeys = JSON.parse(childProcess.spawnSync('node -p "const x = Object.getOwnPropertyNames(global);JSON.stringify(x)"', { shell: true, encoding: 'utf8' }).stdout)
const extraKeys = Object.getOwnPropertyNames(global).filter((key) => {
return !nodeGlobalKeys.includes(key)
})
console.error(`extra keys in electron process: ${extraKeys}`)
}
const allowList = ['__core-js_shared__', 'getSnapshotResult', 'supportTypeScript', 'SuppressedError', 'DisposableStack', 'AsyncDisposableStack', 'Float16Array']
await testAlteringEntryPoint(`(${compareGlobals.toString()})()`, `extra keys in electron process: ${allowList}\nIntegrity check failed with expected stack length 10 but got 12`)
const testTemporarilyRewritingEntryPoint = async () => {
const file = path.join(buildAppDir, 'index.js')
const backupFile = path.join(buildAppDir, 'index.js.bak')
const contents = await fs.readFile(file)
// Backup state
await fs.move(file, backupFile)
// Modify app
await fs.writeFile(file, `console.log("rewritten code");const fs=require('fs');const { join } = require('path');fs.writeFileSync(join(__dirname,'index.js'),fs.readFileSync(join(__dirname,'index.js.bak')));${contents}`)
await runErroringProjectTest(buildAppExecutable, e2e, 'temporarily rewriting index.js', 'Integrity check failed with expected column number 2573 but got')
// Restore original state
await fs.move(backupFile, file, { overwrite: true })
}
await testTemporarilyRewritingEntryPoint()
}
const test = async function (buildAppExecutable, buildAppDir) {
await scaffoldCommonNodeModules()
await Fixtures.scaffoldProject('e2e')
const e2e = Fixtures.projectPath('e2e')
await runSmokeTest(buildAppExecutable)
await runProjectTest(buildAppExecutable, e2e)
await runFailingProjectTest(buildAppExecutable, e2e)
if (!['1', 'true'].includes(process.env.DISABLE_SNAPSHOT_REQUIRE)) {
await runIntegrityTest(buildAppExecutable, buildAppDir, e2e)
await runV8SnapshotProjectTest(buildAppExecutable, e2e)
}
Fixtures.remove()
}
module.exports = {
test,
}
if (require.main === module) {
const buildAppExecutable = path.join(__dirname, `../../build/${os.platform()}-unpacked/Cypress`)
console.log('Script invoked directly, running smoke tests.')
test(buildAppExecutable)
}