diff --git a/packages/config/package.json b/packages/config/package.json index dbd4b15de7..3b8fbb5105 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -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", diff --git a/packages/config/src/ast-utils/addToCypressConfig.ts b/packages/config/src/ast-utils/addToCypressConfig.ts index 313f6682cb..5c5a7896b4 100644 --- a/packages/config/src/ast-utils/addToCypressConfig.ts +++ b/packages/config/src/ast-utils/addToCypressConfig.ts @@ -93,6 +93,7 @@ export interface AddTestingTypeToCypressConfigOptions { info: ASTComponentDefinitionConfig | { testingType: 'e2e' } + projectRoot: string } export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCypressConfigOptions): Promise { @@ -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 = { + + } ` } diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts index bedb325c4c..95f8b23d9d 100644 --- a/packages/config/src/index.ts +++ b/packages/config/src/index.ts @@ -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' diff --git a/packages/config/test/ast-utils/addToCypressConfig.spec.ts b/packages/config/test/ast-utils/addToCypressConfig.spec.ts index 9381b2eb09..3929bdf6d9 100644 --- a/packages/config/test/ast-utils/addToCypressConfig.spec.ts +++ b/packages/config/test/ast-utils/addToCypressConfig.spec.ts @@ -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') diff --git a/packages/data-context/src/actions/WizardActions.ts b/packages/data-context/src/actions/WizardActions.ts index 5f0e4519d8..659cf7a654 100644 --- a/packages/data-context/src/actions/WizardActions.ts +++ b/packages/data-context/src/actions/WizardActions.ts @@ -233,6 +233,7 @@ export class WizardActions { isProjectUsingESModules: this.ctx.lifecycleManager.metaState.isProjectUsingESModules, filePath: configFilePath, info: testingTypeInfo, + projectRoot: this.projectRoot, }) const description = (testingType === 'e2e') diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts index 6f626f628a..e22bfeb217 100644 --- a/packages/data-context/src/sources/migration/codegen.ts +++ b/packages/data-context/src/sources/migration/codegen.ts @@ -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) : '' diff --git a/packages/launchpad/cypress.config.ts b/packages/launchpad/cypress.config.ts index 3993286a9b..b11799991f 100644 --- a/packages/launchpad/cypress.config.ts +++ b/packages/launchpad/cypress.config.ts @@ -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) diff --git a/packages/launchpad/cypress/e2e/scaffold-project.cy.ts b/packages/launchpad/cypress/e2e/scaffold-project.cy.ts index 81b9124197..38efdae2f4 100644 --- a/packages/launchpad/cypress/e2e/scaffold-project.cy.ts +++ b/packages/launchpad/cypress/e2e/scaffold-project.cy.ts @@ -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') + }) }) diff --git a/packages/launchpad/cypress/tasks/uninstallDependenciesInScaffoldedProject.ts b/packages/launchpad/cypress/tasks/uninstallDependenciesInScaffoldedProject.ts new file mode 100644 index 0000000000..f0ce39d8e3 --- /dev/null +++ b/packages/launchpad/cypress/tasks/uninstallDependenciesInScaffoldedProject.ts @@ -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 +}