fix: Don't include defineConfig if cypress isn't available from project root (#22005)

* fix: Don't include defineConfig in generated config if cypress isn't available from project root

* Add e2e test

* Trying to figure out why test fails in CI

* Fix ts error, more specific timeout

* Once more with feeling

Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
This commit is contained in:
Blue F
2022-06-01 14:48:02 -07:00
committed by GitHub
parent 369a865237
commit e12e0d96dd
9 changed files with 125 additions and 33 deletions

View File

@@ -13,7 +13,7 @@
"test": "yarn test-unit",
"test:clean": "find ./test/__fixtures__ -depth -name 'output.*' -type f -exec rm {} \\;",
"test-debug": "yarn test-unit --inspect-brk=5566",
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register 'test/**/*.spec.ts' --exit"
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register 'test/**/*.spec.ts' --exit --timeout 5000"
},
"dependencies": {
"@babel/core": "^7",

View File

@@ -93,6 +93,7 @@ export interface AddTestingTypeToCypressConfigOptions {
info: ASTComponentDefinitionConfig | {
testingType: 'e2e'
}
projectRoot: string
}
export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCypressConfigOptions): Promise<AddToCypressConfigResult> {
@@ -114,7 +115,7 @@ export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCy
// gracefully by adding some default code to use as the AST here, based on the extension
if (!result || result.trim() === '') {
resultStatus = 'ADDED'
result = getEmptyCodeBlock({ outputType: pathExt as OutputExtension, isProjectUsingESModules: options.isProjectUsingESModules })
result = getEmptyCodeBlock({ outputType: pathExt as OutputExtension, isProjectUsingESModules: options.isProjectUsingESModules, projectRoot: options.projectRoot })
}
const toPrint = await addToCypressConfig(options.filePath, result, toAdd)
@@ -133,27 +134,60 @@ export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCy
}
}
// If they are running Cypress that isn't installed in their
// project's node_modules, we don't want to include
// defineConfig(/***/) in their cypress.config.js,
// since it won't exist.
export function defineConfigAvailable (projectRoot: string) {
try {
const cypress = require.resolve('cypress', {
paths: [projectRoot],
})
const api = require(cypress)
return 'defineConfig' in api
} catch (e) {
return false
}
}
type OutputExtension = '.ts' | '.mjs' | '.js'
// Necessary to handle the edge case of them deleting the contents of their Cypress
// config file, just before we merge in the testing type
function getEmptyCodeBlock ({ outputType, isProjectUsingESModules }: { outputType: OutputExtension, isProjectUsingESModules: boolean}) {
if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
return dedent`
import { defineConfig } from 'cypress'
function getEmptyCodeBlock ({ outputType, isProjectUsingESModules, projectRoot }: { outputType: OutputExtension, isProjectUsingESModules: boolean, projectRoot: string}) {
if (defineConfigAvailable(projectRoot)) {
if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
return dedent`
import { defineConfig } from 'cypress'
export default defineConfig({
})
`
}
return dedent`
const { defineConfig } = require('cypress')
module.exports = defineConfig({
export default defineConfig({
})
`
}
return dedent`
const { defineConfig } = require('cypress')
if (outputType === '.ts' || outputType === '.mjs' || isProjectUsingESModules) {
return dedent`
export default {
module.exports = defineConfig({
})
}
`
}
return dedent`
module.exports = {
}
`
}

View File

@@ -2,4 +2,4 @@
// babel transforms, etc. into client-side usage of the config code
export * from './browser'
export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from './ast-utils/addToCypressConfig'
export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions, defineConfigAvailable } from './ast-utils/addToCypressConfig'

View File

@@ -26,6 +26,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
@@ -50,6 +51,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: true,
projectRoot: __dirname,
})
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
@@ -74,6 +76,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
@@ -91,6 +94,52 @@ describe('addToCypressConfig', () => {
expect(result.result).to.eq('ADDED')
})
it('will exclude defineConfig if cypress can\'t be imported from the projectRoot', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/empty.config.js'),
info: {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: '/foo',
})
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
module.exports = {
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
};
`)
expect(result.result).to.eq('ADDED')
})
it('will exclude defineConfig if cypress can\'t be imported from the projectRoot for an ECMA Script project', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/empty.config.js'),
info: {
testingType: 'e2e',
},
isProjectUsingESModules: true,
projectRoot: '/foo',
})
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
export default {
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
},
},
};
`)
expect(result.result).to.eq('ADDED')
})
it('will error if we are unable to add to the config', async () => {
const result = await addTestingTypeToCypressConfig({
filePath: path.join(__dirname, '../__fixtures__/invalid.config.ts'),
@@ -98,6 +147,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})
expect(result.result).to.eq('NEEDS_MERGE')
@@ -111,6 +161,7 @@ describe('addToCypressConfig', () => {
testingType: 'e2e',
},
isProjectUsingESModules: false,
projectRoot: __dirname,
})
expect(result.result).to.eq('NEEDS_MERGE')

View File

@@ -233,6 +233,7 @@ export class WizardActions {
isProjectUsingESModules: this.ctx.lifecycleManager.metaState.isProjectUsingESModules,
filePath: configFilePath,
info: testingTypeInfo,
projectRoot: this.projectRoot,
})
const description = (testingType === 'e2e')

View File

@@ -15,7 +15,7 @@ import { LegacyCypressConfigJson, legacyIntegrationFolder } from '..'
import { parse } from '@babel/parser'
import generate from '@babel/generator'
import _ from 'lodash'
import { getBreakingKeys } from '@packages/config'
import { defineConfigAvailable, getBreakingKeys } from '@packages/config'
const debug = Debug('cypress:data-context:sources:migration:codegen')
@@ -148,24 +148,6 @@ async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot:
return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot)
}
// If they are running an old version of Cypress
// or running Cypress that isn't installed in their
// project's node_modules, we don't want to include
// defineConfig(/***/) in their cypress.config.js,
// since it won't exist.
export function defineConfigAvailable (projectRoot: string) {
try {
const cypress = require.resolve('cypress', {
paths: [projectRoot],
})
const api = require(cypress)
return 'defineConfig' in api
} catch (e) {
return false
}
}
function createCypressConfig (config: ConfigOptions, pluginPath: string | undefined, options: CreateConfigOptions): string {
const globalString = Object.keys(config.global).length > 0 ? `${formatObjectForConfig(config.global)},` : ''
const componentString = options.hasComponentTesting ? createComponentTemplate(config.component) : ''

View File

@@ -1,6 +1,7 @@
import { defineConfig } from 'cypress'
import getenv from 'getenv'
import { snapshotCypressDirectory } from './cypress/tasks/snapshotsScaffold'
import { uninstallDependenciesInScaffoldedProject } from './cypress/tasks/uninstallDependenciesInScaffoldedProject'
const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development')
@@ -41,6 +42,7 @@ export default defineConfig({
on('task', {
snapshotCypressDirectory,
uninstallDependenciesInScaffoldedProject,
})
return await e2ePluginSetup(on, config)

View File

@@ -158,4 +158,17 @@ describe('scaffolding new projects', { defaultCommandTimeout: 7000 }, () => {
scaffoldAndOpenCTProject({ name: 'pristine', framework: 'Create React App', removeFixturesFolder: false })
assertScaffoldedFilesAreCorrect({ language, testingType: 'component', ctFramework: 'Create React App (v5)', customDirectory: 'without-fixtures' })
})
it('generates valid config file for pristine project without cypress installed', () => {
cy.scaffoldProject('pristine')
cy.openProject('pristine')
cy.withCtx((ctx) => ctx.currentProject).then((currentProject) => {
cy.task('uninstallDependenciesInScaffoldedProject', { currentProject })
})
cy.visitLaunchpad()
cy.contains('button', cy.i18n.testingType.e2e.name).click()
cy.contains('button', cy.i18n.setupPage.step.continue).click()
cy.contains('h1', cy.i18n.setupPage.testingCard.chooseABrowser).should('be.visible')
})
})

View File

@@ -0,0 +1,9 @@
import fs from 'fs'
import path from 'path'
export async function uninstallDependenciesInScaffoldedProject ({ currentProject }) {
// @ts-ignore
fs.rmdirSync(path.resolve(currentProject, '../node_modules'), { recursive: true, force: true })
return null
}