diff --git a/packages/data-context/src/actions/MigrationActions.ts b/packages/data-context/src/actions/MigrationActions.ts index a63610d526..ac0eeeac6c 100644 --- a/packages/data-context/src/actions/MigrationActions.ts +++ b/packages/data-context/src/actions/MigrationActions.ts @@ -1,22 +1,166 @@ +/* eslint-disable no-dupe-class-members */ import path from 'path' +import { fork } from 'child_process' +import type { ForkOptions } from 'child_process' import assert from 'assert' import type { DataContext } from '..' import { cleanUpIntegrationFolder, formatConfig, + LegacyCypressConfigJson, moveSpecFiles, NonStandardMigrationError, SpecToMove, - supportFilesForMigration, } from '../sources' +import { + tryGetDefaultLegacyPluginsFile, + supportFilesForMigration, + hasSpecFile, + getStepsForMigration, + getIntegrationFolder, + isDefaultTestFiles, + getComponentTestFilesGlobs, + getComponentFolder, + getIntegrationTestFilesGlobs, + getSpecPattern, +} from '../sources/migration' +import { makeCoreData } from '../data' +import { LegacyPluginsIpc } from '../data/LegacyPluginsIpc' + +export async function processConfigViaLegacyPlugins (projectRoot: string, legacyConfig: LegacyCypressConfigJson): Promise { + const pluginFile = legacyConfig.pluginsFile ?? await tryGetDefaultLegacyPluginsFile(projectRoot) + + return new Promise((resolve, reject) => { + // couldn't find a pluginsFile + // just bail with initial config + if (!pluginFile) { + return resolve(legacyConfig) + } + + const cwd = path.join(projectRoot, pluginFile) + + const childOptions: ForkOptions = { + stdio: 'inherit', + cwd: path.dirname(cwd), + env: process.env, + } + + const configProcessArgs = ['--projectRoot', projectRoot, '--file', cwd] + const CHILD_PROCESS_FILE_PATH = require.resolve('@packages/server/lib/plugins/child/require_async_child') + const ipc = new LegacyPluginsIpc(fork(CHILD_PROCESS_FILE_PATH, configProcessArgs, childOptions)) + + ipc.on('ready', () => { + ipc.send('loadLegacyPlugins', legacyConfig) + }) + + ipc.on('loadLegacyPlugins:reply', (modifiedLegacyConfig) => { + resolve(modifiedLegacyConfig) + ipc.childProcess.kill() + }) + + ipc.on('loadLegacyPlugins:error', (error) => { + reject(error) + ipc.childProcess.kill() + }) + + ipc.on('childProcess:unhandledError', (error) => { + reject(error) + ipc.childProcess.kill() + }) + }) +} export class MigrationActions { constructor (private ctx: DataContext) { } + async initialize (config: LegacyCypressConfigJson) { + const legacyConfigForMigration = await this.setLegacyConfigForMigration(config) + + // for testing mainly, we want to ensure the flags are reset each test + this.resetFlags() + + if (!this.ctx.currentProject || !legacyConfigForMigration) { + throw Error('cannot do migration without currentProject!') + } + + await this.initializeFlags() + + const legacyConfigFileExist = await this.ctx.lifecycleManager.checkIfLegacyConfigFileExist() + const filteredSteps = await getStepsForMigration(this.ctx.currentProject, legacyConfigForMigration, Boolean(legacyConfigFileExist)) + + this.ctx.update((coreData) => { + if (!filteredSteps[0]) { + throw Error(`Impossible to initialize a migration. No steps fit the configuration of this project.`) + } + + coreData.migration.filteredSteps = filteredSteps + coreData.migration.step = filteredSteps[0] + }) + } + + /** + * Figure out all the data required for the migration UI. + * This drives which migration steps need be shown and performed. + */ + private async initializeFlags () { + const legacyConfigForMigration = this.ctx.coreData.migration.legacyConfigForMigration + + if (!this.ctx.currentProject || !legacyConfigForMigration) { + throw Error('Need currentProject to do migration') + } + + const integrationFolder = getIntegrationFolder(legacyConfigForMigration) + const integrationTestFiles = getIntegrationTestFilesGlobs(legacyConfigForMigration) + + const hasCustomIntegrationFolder = getIntegrationFolder(legacyConfigForMigration) !== 'cypress/integration' + const hasCustomIntegrationTestFiles = !isDefaultTestFiles(legacyConfigForMigration, 'integration') + + let hasE2ESpec = integrationFolder + ? await hasSpecFile(this.ctx.currentProject, integrationFolder, integrationTestFiles) + : false + + // if we don't find specs in the 9.X scope, + // let's check already migrated files. + // this allows users to stop migration halfway, + // then to pick up where they left migration off + if (!hasE2ESpec && (!hasCustomIntegrationTestFiles || !hasCustomIntegrationFolder)) { + const newE2eSpecPattern = getSpecPattern(legacyConfigForMigration, 'e2e') + + hasE2ESpec = await hasSpecFile(this.ctx.currentProject, '', newE2eSpecPattern) + } + + const componentFolder = getComponentFolder(legacyConfigForMigration) + const componentTestFiles = getComponentTestFilesGlobs(legacyConfigForMigration) + + const hasCustomComponentFolder = componentFolder !== 'cypress/component' + const hasCustomComponentTestFiles = !isDefaultTestFiles(legacyConfigForMigration, 'component') + + const hasComponentTesting = componentFolder + ? await hasSpecFile(this.ctx.currentProject, componentFolder, componentTestFiles) + : false + + this.ctx.update((coreData) => { + coreData.migration.flags = { + hasCustomIntegrationFolder, + hasCustomIntegrationTestFiles, + hasCustomComponentFolder, + hasCustomComponentTestFiles, + hasCustomSupportFile: false, + hasComponentTesting, + hasE2ESpec, + hasPluginsFile: true, + } + }) + } + + get configFileNameAfterMigration () { + return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.metaState.hasTypescript ? 'ts' : 'js'}`) + } + async createConfigFile () { const config = await this.ctx.migration.createConfigString() - this.ctx.lifecycleManager.setConfigFilePath(this.ctx.migration.configFileNameAfterMigration) + this.ctx.lifecycleManager.setConfigFilePath(this.configFileNameAfterMigration) await this.ctx.fs.writeFile(this.ctx.lifecycleManager.configFilePath, config).catch((error) => { throw error @@ -31,8 +175,15 @@ export class MigrationActions { this.ctx.modeOptions.configFile = this.ctx.migration.configFileNameAfterMigration } - initialize () { - return this.ctx.migration.initialize() + async setLegacyConfigForMigration (config: LegacyCypressConfigJson) { + assert(this.ctx.currentProject) + const legacyConfigForMigration = await processConfigViaLegacyPlugins(this.ctx.currentProject, config) + + this.ctx.update((coreData) => { + coreData.migration.legacyConfigForMigration = legacyConfigForMigration + }) + + return legacyConfigForMigration } async renameSpecsFolder () { @@ -98,8 +249,8 @@ export class MigrationActions { } async nextStep () { - const filteredSteps = this.ctx.migration.filteredSteps - const index = filteredSteps.indexOf(this.ctx.migration.step) + const filteredSteps = this.ctx.coreData.migration.filteredSteps + const index = filteredSteps.indexOf(this.ctx.coreData.migration.step) if (index === -1) { throw new Error('Invalid step') @@ -111,7 +262,9 @@ export class MigrationActions { const nextStep = filteredSteps[nextIndex] if (nextStep) { - this.ctx.migration.setStep(nextStep) + this.ctx.update((coreData) => { + coreData.migration.step = nextStep + }) } } else { await this.finishReconfigurationWizard() @@ -152,4 +305,12 @@ export class MigrationActions { throw Error(`Expected ${actual} to equal ${expected}`) } } + + resetFlags () { + this.ctx.update((coreData) => { + const defaultFlags = makeCoreData().migration.flags + + coreData.migration.flags = defaultFlags + }) + } } diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index 5221e70c29..bc3f2e7131 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -181,7 +181,7 @@ export class ProjectActions { } if (args.open) { - await this.setCurrentProject(projectRoot) + this.setCurrentProject(projectRoot).catch(this.ctx.onError) } } diff --git a/packages/data-context/src/data/LegacyPluginsIpc.ts b/packages/data-context/src/data/LegacyPluginsIpc.ts new file mode 100644 index 0000000000..9f0d5111b0 --- /dev/null +++ b/packages/data-context/src/data/LegacyPluginsIpc.ts @@ -0,0 +1,35 @@ +/* eslint-disable no-dupe-class-members */ +import type { ChildProcess } from 'child_process' +import EventEmitter from 'events' +import type { CypressError } from '@packages/errors' +import type { LegacyCypressConfigJson } from '../sources' + +export class LegacyPluginsIpc extends EventEmitter { + constructor (readonly childProcess: ChildProcess) { + super() + childProcess.on('message', (msg: { event: string, args: any[] }) => { + this.emit(msg.event, ...msg.args) + }) + + childProcess.once('disconnect', () => { + this.emit('disconnect') + }) + } + + send(event: 'loadLegacyPlugins', legacyConfig: LegacyCypressConfigJson): boolean + send (event: string, ...args: any[]) { + if (this.childProcess.killed) { + return false + } + + return this.childProcess.send({ event, args }) + } + + on(event: 'ready', listener: () => void): this + on(event: 'loadLegacyPlugins:error', listener: (error: CypressError) => void): this + on(event: 'childProcess:unhandledError', listener: (legacyConfig: LegacyCypressConfigJson) => void): this + on(event: 'loadLegacyPlugins:reply', listener: (legacyConfig: LegacyCypressConfigJson) => void): this + on (evt: string, listener: (...args: any[]) => void) { + return super.on(evt, listener) + } +} diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index 78d37ea81d..12eb9731ab 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -22,6 +22,7 @@ import { LoadConfigReply, SetupNodeEventsReply, ProjectConfigIpc, IpcHandler } f import assert from 'assert' import type { AllModeOptions, BreakingErrResult, BreakingOption, FoundBrowser, FullConfig, TestingType } from '@packages/types' import { autoBindDebug } from '../util/autoBindDebug' +import type { LegacyCypressConfigJson } from '../sources' const debug = debugLib(`cypress:lifecycle:ProjectLifecycleManager`) @@ -118,6 +119,7 @@ export class ProjectLifecycleManager { private _cachedFullConfig: FullConfig | undefined private _projectMetaState: ProjectMetaState = { ...PROJECT_META_STATE } + _pendingMigrationInitialize?: pDefer.DeferredPromise constructor (private ctx: DataContext) { this._handlers = this.ctx._apis.configApi.getServerPluginHandlers() @@ -259,6 +261,7 @@ export class ProjectLifecycleManager { this._projectRoot = projectRoot this._initializedProject = undefined + this._pendingMigrationInitialize = pDefer() this.legacyPluginGuard() Promise.resolve(this.ctx.browser.machineBrowsers()).catch(this.onLoadError) this.verifyProjectRoot(projectRoot) @@ -272,6 +275,22 @@ export class ProjectLifecycleManager { const { needsCypressJsonMigration } = this.refreshMetaState() + const legacyConfigPatah = path.join(projectRoot, this.legacyConfigFile) + + if (needsCypressJsonMigration && !this.ctx.isRunMode && this.ctx.fs.existsSync(legacyConfigPatah)) { + // we run the legacy plugins/index.js in a child process + // and mutate the config based on the return value for migration + // only used in open mode (cannot migrate via terminal) + const legacyConfig = this.ctx.fs.readJsonSync(legacyConfigPatah) as LegacyCypressConfigJson + + // should never throw, unless there existing pluginsFile errors out, + // in which case they are attempting to migrate an already broken project. + this.ctx.actions.migration.initialize(legacyConfig) + .then(this._pendingMigrationInitialize?.resolve) + .finally(() => this._pendingMigrationInitialize = undefined) + .catch(this.onLoadError) + } + this.configFileWarningCheck() if (this.metaState.hasValidConfigFile) { diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 735986d555..6afc950c7d 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -1,4 +1,4 @@ -import type { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName } from '@packages/types' +import { FoundBrowser, Editor, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName, MIGRATION_STEPS, MigrationStep } from '@packages/types' import type { Bundler, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config' import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' import type { App, BrowserWindow } from 'electron' @@ -6,6 +6,7 @@ import type { ChildProcess } from 'child_process' import type { SocketIOServer } from '@packages/socket' import type { Server } from 'http' import type { ErrorWrapperSource } from '@packages/errors' +import type { LegacyCypressConfigJson } from '../sources' export type Maybe = T | null | undefined @@ -72,9 +73,23 @@ export interface WizardDataShape { detectedFramework: typeof FRONTEND_FRAMEWORKS[number]['type'] | null } -export interface MigrationDataShape{ +export interface MigrationDataShape { // TODO: have the model of migration here - step: NexusGenEnums['MigrationStepEnum'] + step: MigrationStep + legacyConfigForMigration?: LegacyCypressConfigJson | null + filteredSteps: MigrationStep[] + flags: { + hasCustomIntegrationFolder: boolean + hasCustomIntegrationTestFiles: boolean + + hasCustomComponentFolder: boolean + hasCustomComponentTestFiles: boolean + + hasCustomSupportFile: boolean + hasComponentTesting: boolean + hasE2ESpec: boolean + hasPluginsFile: boolean + } } export interface ElectronShape { @@ -115,7 +130,7 @@ export interface CoreDataShape { currentProject: string | null currentTestingType: TestingType | null wizard: WizardDataShape - migration: MigrationDataShape | null + migration: MigrationDataShape user: AuthenticatedUserShape | null electron: ElectronShape authState: AuthStateShape @@ -171,6 +186,18 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa }, migration: { step: 'renameAuto', + legacyConfigForMigration: null, + filteredSteps: [...MIGRATION_STEPS], + flags: { + hasCustomIntegrationFolder: false, + hasCustomIntegrationTestFiles: false, + hasCustomComponentFolder: false, + hasCustomComponentTestFiles: false, + hasCustomSupportFile: false, + hasComponentTesting: true, + hasE2ESpec: true, + hasPluginsFile: true, + }, }, warnings: [], chosenBrowser: null, diff --git a/packages/data-context/src/sources/MigrationDataSource.ts b/packages/data-context/src/sources/MigrationDataSource.ts index a793e7b8da..5c94fd3f40 100644 --- a/packages/data-context/src/sources/MigrationDataSource.ts +++ b/packages/data-context/src/sources/MigrationDataSource.ts @@ -1,26 +1,18 @@ -import { TestingType, MIGRATION_STEPS } from '@packages/types' +import type { TestingType } from '@packages/types' import type chokidar from 'chokidar' -import path from 'path' import type { DataContext } from '..' import { createConfigString, initComponentTestingMigration, ComponentTestingMigrationStatus, - tryGetDefaultLegacyPluginsFile, supportFilesForMigration, - OldCypressConfig, - hasSpecFile, getSpecs, applyMigrationTransform, - getStepsForMigration, shouldShowRenameSupport, getIntegrationFolder, - getPluginsFile, isDefaultTestFiles, getComponentTestFilesGlobs, getComponentFolder, - getIntegrationTestFilesGlobs, - getSpecPattern, } from './migration' import type { FilePart } from './migration/format' @@ -28,6 +20,17 @@ import Debug from 'debug' const debug = Debug('cypress:data-context:sources:MigrationDataSource') +export type LegacyCypressConfigJson = Partial<{ + component: Omit + e2e: Omit + pluginsFile: string | false + supportFile: string | false + componentFolder: string | false + integrationFolder: string + testFiles: string | string[] + ignoreTestFiles: string | string[] +}> + export interface MigrationFile { testingType: TestingType before: { @@ -40,81 +43,25 @@ export interface MigrationFile { } } -type MIGRATION_STEP = typeof MIGRATION_STEPS[number] - -const flags = { - hasCustomIntegrationFolder: false, - hasCustomIntegrationTestFiles: false, - - hasCustomComponentFolder: false, - hasCustomComponentTestFiles: false, - - hasCustomSupportFile: false, - hasComponentTesting: true, - hasE2ESpec: true, - hasPluginsFile: true, -} as const - export class MigrationDataSource { - private _config: OldCypressConfig | null = null - private _step: MIGRATION_STEP = 'renameAuto' - filteredSteps: MIGRATION_STEP[] = MIGRATION_STEPS.filter(() => true) - - hasCustomIntegrationFolder: boolean = flags.hasCustomIntegrationFolder - hasCustomIntegrationTestFiles: boolean = flags.hasCustomIntegrationTestFiles - - hasCustomComponentFolder: boolean = flags.hasCustomComponentFolder - hasCustomComponentTestFiles: boolean = flags.hasCustomComponentTestFiles - - hasCustomSupportFile: boolean = flags.hasCustomSupportFile - hasComponentTesting: boolean = flags.hasComponentTesting - hasE2ESpec: boolean = flags.hasE2ESpec - hasPluginsFile: boolean = flags.hasPluginsFile - private componentTestingMigrationWatcher: chokidar.FSWatcher | null = null componentTestingMigrationStatus?: ComponentTestingMigrationStatus - private _oldConfigPromise: Promise | null = null constructor (private ctx: DataContext) { } - async initialize () { - // for testing mainly, we want to ensure the flags are reset each test - this.resetFlags() - - if (!this.ctx.currentProject) { - throw Error('cannot do migration without currentProject!') + get legacyConfig () { + if (!this.ctx.coreData.migration.legacyConfigForMigration) { + throw Error(`Expected _legacyConfig to be set. Did you forget to call MigrationDataSource#initialize?`) } - this._config = null - this._oldConfigPromise = null - const config = await this.parseCypressConfig() - - await this.initializeFlags() - - const legacyConfigFileExist = await this.ctx.lifecycleManager.checkIfLegacyConfigFileExist() - - this.filteredSteps = await getStepsForMigration(this.ctx.currentProject, config, Boolean(legacyConfigFileExist)) - - if (!this.filteredSteps[0]) { - throw Error(`Impossible to initialize a migration. No steps fit the configuration of this project.`) - } - - this.setStep(this.filteredSteps[0]) - } - - private resetFlags () { - for (const [k, v] of Object.entries(flags)) { - this[k as keyof typeof flags] = v - } + return this.ctx.coreData.migration.legacyConfigForMigration } async getComponentTestingMigrationStatus () { debug('getComponentTestingMigrationStatus: start') - const config = await this.parseCypressConfig() + const componentFolder = getComponentFolder(this.legacyConfig) - const componentFolder = getComponentFolder(config) - - if (!config || !this.ctx.currentProject) { + if (!this.legacyConfig || !this.ctx.currentProject) { throw Error('Need currentProject and config to continue') } @@ -145,7 +92,7 @@ export class MigrationDataSource { const { status, watcher } = await initComponentTestingMigration( this.ctx.currentProject, componentFolder, - getComponentTestFilesGlobs(config), + getComponentTestFilesGlobs(this.legacyConfig), onFileMoved, ) @@ -166,10 +113,8 @@ export class MigrationDataSource { throw Error('Need this.ctx.currentProject') } - const config = await this.parseCypressConfig() - - debug('supportFilesForMigrationGuide: config %O', config) - if (!await shouldShowRenameSupport(this.ctx.currentProject, config)) { + debug('supportFilesForMigrationGuide: config %O', this.legacyConfig) + if (!await shouldShowRenameSupport(this.ctx.currentProject, this.legacyConfig)) { return null } @@ -195,13 +140,11 @@ export class MigrationDataSource { throw Error(`Need this.ctx.projectRoot!`) } - const config = await this.parseCypressConfig() - - const specs = await getSpecs(this.ctx.currentProject, config) + const specs = await getSpecs(this.ctx.currentProject, this.legacyConfig) const canBeAutomaticallyMigrated: MigrationFile[] = specs.integration.map(applyMigrationTransform).filter((spec) => spec.before.relative !== spec.after.relative) - const defaultComponentPattern = isDefaultTestFiles(await this.parseCypressConfig(), 'component') + const defaultComponentPattern = isDefaultTestFiles(this.legacyConfig, 'component') // Can only migration component specs if they use the default testFiles pattern. if (defaultComponentPattern) { @@ -211,12 +154,6 @@ export class MigrationDataSource { return canBeAutomaticallyMigrated } - async getConfig () { - const config = await this.parseCypressConfig() - - return JSON.stringify(config, null, 2) - } - async createConfigString () { if (!this.ctx.currentProject) { throw Error('Need currentProject!') @@ -224,136 +161,30 @@ export class MigrationDataSource { const { hasTypescript } = this.ctx.lifecycleManager.metaState - const config = await this.parseCypressConfig() - - return createConfigString(config, { - hasComponentTesting: this.hasComponentTesting, - hasE2ESpec: this.hasE2ESpec, - hasPluginsFile: this.hasPluginsFile, + return createConfigString(this.legacyConfig, { + hasComponentTesting: this.ctx.coreData.migration.flags.hasComponentTesting, + hasE2ESpec: this.ctx.coreData.migration.flags.hasE2ESpec, + hasPluginsFile: this.ctx.coreData.migration.flags.hasPluginsFile, projectRoot: this.ctx.currentProject, hasTypescript, }) } async integrationFolder () { - const config = await this.parseCypressConfig() - - return getIntegrationFolder(config) + return getIntegrationFolder(this.legacyConfig) } async componentFolder () { - const config = await this.parseCypressConfig() - - return getComponentFolder(config) - } - - private async parseCypressConfig (): Promise { - if (this._config) { - return this._config - } - - // avoid reading the same file over and over again before it was finished reading - if (this.ctx.lifecycleManager.metaState.hasLegacyCypressJson && !this._oldConfigPromise) { - const cfgPath = path.join(this.ctx.lifecycleManager?.projectRoot, this.ctx.lifecycleManager.legacyConfigFile) - - this._oldConfigPromise = this.ctx.file.readJsonFile(cfgPath) as Promise - } - - if (this._oldConfigPromise) { - this._config = await this._oldConfigPromise - - this._oldConfigPromise = null - - return this._config - } - - return {} - } - - private async initializeFlags () { - if (!this.ctx.currentProject) { - throw Error('Need currentProject to do migration') - } - - const config = await this.parseCypressConfig() - - const integrationFolder = getIntegrationFolder(config) - const integrationTestFiles = getIntegrationTestFilesGlobs(config) - - this.hasCustomIntegrationFolder = getIntegrationFolder(config) !== 'cypress/integration' - this.hasCustomIntegrationTestFiles = !isDefaultTestFiles(config, 'integration') - - if (integrationFolder === false) { - this.hasE2ESpec = false - } else { - this.hasE2ESpec = await hasSpecFile( - this.ctx.currentProject, - integrationFolder, - integrationTestFiles, - ) - - // if we don't find specs in the 9.X scope, - // let's check already migrated files. - // this allows users to stop migration halfway, - // then to pick up where they left migration off - if (!this.hasE2ESpec && (!this.hasCustomIntegrationTestFiles || !this.hasCustomIntegrationFolder)) { - const newE2eSpecPattern = getSpecPattern(config, 'e2e') - - this.hasE2ESpec = await hasSpecFile( - this.ctx.currentProject, - '', - newE2eSpecPattern, - ) - } - } - - const componentFolder = getComponentFolder(config) - const componentTestFiles = getComponentTestFilesGlobs(config) - - this.hasCustomComponentFolder = componentFolder !== 'cypress/component' - this.hasCustomComponentTestFiles = !isDefaultTestFiles(config, 'component') - - if (componentFolder === false) { - this.hasComponentTesting = false - } else { - this.hasComponentTesting = await hasSpecFile( - this.ctx.currentProject, - componentFolder, - componentTestFiles, - ) - - // We cannot check already migrated component specs since it would pick up e2e specs as well - // the default specPattern for CT is **/*.cy.js. - // since component testing has to be re-installed anyway, we can just skip this - } - - const pluginsFileMissing = ( - (config.e2e?.pluginsFile ?? undefined) === undefined && - config.pluginsFile === undefined && - !await tryGetDefaultLegacyPluginsFile(this.ctx.currentProject) - ) - - if (getPluginsFile(config) === false || pluginsFileMissing) { - this.hasPluginsFile = false - } - } - - get step (): MIGRATION_STEP { - return this._step + return getComponentFolder(this.legacyConfig) } async closeManualRenameWatcher () { if (this.componentTestingMigrationWatcher) { - debug('setStep: stopping watcher') await this.componentTestingMigrationWatcher.close() this.componentTestingMigrationWatcher = null } } - setStep (step: MIGRATION_STEP) { - this._step = step - } - get configFileNameAfterMigration () { return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.metaState.hasTypescript ? 'ts' : 'js'}`) } diff --git a/packages/data-context/src/sources/migration/autoRename.ts b/packages/data-context/src/sources/migration/autoRename.ts index ebe88d4421..7878e9caf6 100644 --- a/packages/data-context/src/sources/migration/autoRename.ts +++ b/packages/data-context/src/sources/migration/autoRename.ts @@ -8,10 +8,10 @@ import { getIntegrationFolder, getIntegrationTestFilesGlobs, isDefaultTestFiles, - OldCypressConfig, regexps, } from '.' import type { MigrationFile } from '../MigrationDataSource' +import type { LegacyCypressConfigJson } from '..' export interface MigrationSpec { relative: string @@ -102,7 +102,7 @@ export function applyMigrationTransform ( } } -export async function getSpecs (projectRoot: string, config: OldCypressConfig): Promise { +export async function getSpecs (projectRoot: string, config: LegacyCypressConfigJson): Promise { const integrationFolder = getIntegrationFolder(config) const integrationTestFiles = getIntegrationTestFilesGlobs(config) @@ -112,21 +112,21 @@ export async function getSpecs (projectRoot: string, config: OldCypressConfig): let integrationSpecs: MigrationSpec[] = [] let componentSpecs: MigrationSpec[] = [] - const globs = integrationFolder === false - ? [] - : integrationFolder === 'cypress/integration' + const globs = integrationFolder + ? integrationFolder === 'cypress/integration' ? ['**/*.{js,ts,jsx,tsx,coffee}'].map((glob) => `${integrationFolder}/${glob}`) : integrationTestFiles.map((glob) => `${integrationFolder}/${glob}`) + : [] - let specs = integrationFolder === false - ? [] - : (await globby(globs, { onlyFiles: true, cwd: projectRoot })) + let specs = integrationFolder + ? (await globby(globs, { onlyFiles: true, cwd: projectRoot })) + : [] const fullyCustom = integrationFolder !== 'cypress/integration' && !isDefaultTestFiles(config, 'integration') // we cannot do a migration if either integrationFolder is false, // or if both the integrationFolder and testFiles are custom. - if (integrationFolder === false || fullyCustom) { + if (fullyCustom) { integrationSpecs = [] } else { integrationSpecs = specs.map((relative) => { diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts index 8485685a2b..4d6acdf4c7 100644 --- a/packages/data-context/src/sources/migration/codegen.ts +++ b/packages/data-context/src/sources/migration/codegen.ts @@ -12,6 +12,7 @@ import { toPosix } from '../../util' import Debug from 'debug' import dedent from 'dedent' import { hasDefaultExport } from './parserUtils' +import type { LegacyCypressConfigJson } from '..' const debug = Debug('cypress:data-context:sources:migration:codegen') @@ -26,25 +27,6 @@ type ResolvedConfigOptions = Cypress.ResolvedConfigOptions & { ignoreTestFiles: string | string[] } -/** - * config format pre-10.0 - */ -export interface OldCypressConfig { - // limited subset of properties, used for unit tests - viewportWidth?: number - baseUrl?: string - retries?: number - - component?: Omit - e2e?: Omit - pluginsFile?: string | false - supportFile?: string | false - componentFolder?: string | false - integrationFolder?: string | false - testFiles?: string | string[] - ignoreTestFiles?: string -} - export class NonStandardMigrationError extends Error { constructor (fileType: 'support' | 'config') { super() @@ -60,7 +42,7 @@ export interface CreateConfigOptions { hasTypescript: boolean } -export async function createConfigString (cfg: OldCypressConfig, options: CreateConfigOptions) { +export async function createConfigString (cfg: LegacyCypressConfigJson, options: CreateConfigOptions) { const newConfig = reduceConfig(cfg) const relativePluginPath = await getPluginRelativePath(cfg, options.projectRoot) @@ -156,8 +138,8 @@ export async function initComponentTestingMigration ( }) } -async function getPluginRelativePath (cfg: OldCypressConfig, projectRoot: string): Promise { - return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot) || '' +async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot: string): Promise { + return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot) } // If they are running an old version of Cypress @@ -178,7 +160,7 @@ function defineConfigAvailable (projectRoot: string) { } } -function createCypressConfig (config: ConfigOptions, pluginPath: string, options: CreateConfigOptions): string { +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) : '' const e2eString = options.hasE2ESpec @@ -212,8 +194,8 @@ function formatObjectForConfig (obj: Record) { return JSON.stringify(obj, null, 2).replace(/^[{]|[}]$/g, '') // remove opening and closing {} } -function createE2ETemplate (pluginPath: string, createConfigOptions: CreateConfigOptions, options: Record) { - if (!createConfigOptions.hasPluginsFile) { +function createE2ETemplate (pluginPath: string | undefined, createConfigOptions: CreateConfigOptions, options: Record) { + if (!createConfigOptions.hasPluginsFile || !pluginPath) { return dedent` e2e: { setupNodeEvents(on, config) {},${formatObjectForConfig(options)} @@ -357,7 +339,7 @@ export function renameSupportFilePath (relative: string) { return relative.slice(0, res.index) + relative.slice(res.index).replace(res.groups.supportFileName, 'e2e') } -export function reduceConfig (cfg: OldCypressConfig): ConfigOptions { +export function reduceConfig (cfg: LegacyCypressConfigJson): ConfigOptions { const excludedFields = ['pluginsFile', '$schema'] return Object.entries(cfg).reduce((acc, [key, val]) => { @@ -446,7 +428,7 @@ export function reduceConfig (cfg: OldCypressConfig): ConfigOptions { }, { global: {}, e2e: {}, component: {} }) } -export function getSpecPattern (cfg: OldCypressConfig, testType: TestingType) { +export function getSpecPattern (cfg: LegacyCypressConfigJson, testType: TestingType) { const specPattern = cfg[testType]?.testFiles ?? cfg.testFiles ?? '**/*.cy.{js,jsx,ts,tsx}' const customComponentFolder = cfg.component?.componentFolder ?? cfg.componentFolder ?? null diff --git a/packages/data-context/src/sources/migration/shouldShowSteps.ts b/packages/data-context/src/sources/migration/shouldShowSteps.ts index 55ba754ab3..c6c70f5286 100644 --- a/packages/data-context/src/sources/migration/shouldShowSteps.ts +++ b/packages/data-context/src/sources/migration/shouldShowSteps.ts @@ -1,9 +1,10 @@ import globby from 'globby' import path from 'path' import { MIGRATION_STEPS } from '@packages/types' -import { applyMigrationTransform, getSpecs, OldCypressConfig, tryGetDefaultLegacySupportFile } from '.' +import { applyMigrationTransform, getSpecs, tryGetDefaultLegacySupportFile } from '.' +import type { LegacyCypressConfigJson } from '..' -function getTestFilesGlobs (config: OldCypressConfig, type: 'component' | 'integration'): string[] { +function getTestFilesGlobs (config: LegacyCypressConfigJson, type: 'component' | 'integration'): string[] { // super awkward how we call it integration tests, but the key to override // the config is `e2e` const k = type === 'component' ? 'component' : 'e2e' @@ -17,15 +18,15 @@ function getTestFilesGlobs (config: OldCypressConfig, type: 'component' | 'integ return ['**/*.{js,ts,jsx,tsx,coffee}'] } -export function getIntegrationTestFilesGlobs (config: OldCypressConfig): string[] { +export function getIntegrationTestFilesGlobs (config: LegacyCypressConfigJson): string[] { return getTestFilesGlobs(config, 'integration') } -export function getComponentTestFilesGlobs (config: OldCypressConfig): string[] { +export function getComponentTestFilesGlobs (config: LegacyCypressConfigJson): string[] { return getTestFilesGlobs(config, 'component') } -export function isDefaultTestFiles (config: OldCypressConfig, type: 'component' | 'integration') { +export function isDefaultTestFiles (config: LegacyCypressConfigJson, type: 'component' | 'integration') { const testFiles = type === 'component' ? getComponentTestFilesGlobs(config) : getIntegrationTestFilesGlobs(config) @@ -33,7 +34,7 @@ export function isDefaultTestFiles (config: OldCypressConfig, type: 'component' return testFiles.length === 1 && testFiles[0] === '**/*.{js,ts,jsx,tsx,coffee}' } -export function getPluginsFile (config: OldCypressConfig) { +export function getPluginsFile (config: LegacyCypressConfigJson) { if (config.e2e?.pluginsFile === false || config.pluginsFile === false) { return false } @@ -41,15 +42,11 @@ export function getPluginsFile (config: OldCypressConfig) { return config.e2e?.pluginsFile ?? config.pluginsFile ?? 'cypress/plugins/index.js' } -export function getIntegrationFolder (config: OldCypressConfig) { - if (config.e2e?.integrationFolder === false || config.integrationFolder === false) { - return false - } - +export function getIntegrationFolder (config: LegacyCypressConfigJson) { return config.e2e?.integrationFolder ?? config.integrationFolder ?? 'cypress/integration' } -export function getComponentFolder (config: OldCypressConfig) { +export function getComponentFolder (config: LegacyCypressConfigJson) { if (config.component?.componentFolder === false || config.componentFolder === false) { return false } @@ -63,7 +60,7 @@ async function hasSpecFiles (projectRoot: string, dir: string, testFilesGlob: st return f.length > 0 } -export async function shouldShowAutoRenameStep (projectRoot: string, config: OldCypressConfig) { +export async function shouldShowAutoRenameStep (projectRoot: string, config: LegacyCypressConfigJson) { const specsToAutoMigrate = await getSpecs(projectRoot, config) const integrationCleaned = specsToAutoMigrate.integration.filter((spec) => { @@ -82,7 +79,7 @@ export async function shouldShowAutoRenameStep (projectRoot: string, config: Old return integrationCleaned.length > 0 || componentCleaned.length > 0 } -async function anyComponentSpecsExist (projectRoot: string, config: OldCypressConfig) { +async function anyComponentSpecsExist (projectRoot: string, config: LegacyCypressConfigJson) { const componentFolder = getComponentFolder(config) if (componentFolder === false) { @@ -94,13 +91,9 @@ async function anyComponentSpecsExist (projectRoot: string, config: OldCypressCo return hasSpecFiles(projectRoot, componentFolder, componentTestFiles) } -async function anyIntegrationSpecsExist (projectRoot: string, config: OldCypressConfig) { +async function anyIntegrationSpecsExist (projectRoot: string, config: LegacyCypressConfigJson) { const integrationFolder = getIntegrationFolder(config) - if (integrationFolder === false) { - return false - } - const integrationTestFiles = getIntegrationTestFilesGlobs(config) return hasSpecFiles(projectRoot, integrationFolder, integrationTestFiles) @@ -111,7 +104,7 @@ async function anyIntegrationSpecsExist (projectRoot: string, config: OldCypress // Also, if there are no **no** integration specs, we are doing a CT only migration, // in which case we don't migrate the supportFile - they'll make a new support/component.js // when they set CT up. -export async function shouldShowRenameSupport (projectRoot: string, config: OldCypressConfig) { +export async function shouldShowRenameSupport (projectRoot: string, config: LegacyCypressConfigJson) { if (!await anyIntegrationSpecsExist(projectRoot, config)) { return false } @@ -140,7 +133,7 @@ export async function shouldShowRenameSupport (projectRoot: string, config: OldC // if they have component testing configured using the defaults, they will need to // rename/move their specs. -async function shouldShowRenameManual (projectRoot: string, config: OldCypressConfig) { +async function shouldShowRenameManual (projectRoot: string, config: LegacyCypressConfigJson) { const componentFolder = getComponentFolder(config) const usingAllDefaults = componentFolder === 'cypress/component' && isDefaultTestFiles(config, 'component') @@ -152,11 +145,16 @@ async function shouldShowRenameManual (projectRoot: string, config: OldCypressCo return anyComponentSpecsExist(projectRoot, config) } +// All projects must move from cypress.json to cypress.config.js! +export function shouldShowConfigFileStep (config: LegacyCypressConfigJson) { + return true +} + export type Step = typeof MIGRATION_STEPS[number] export async function getStepsForMigration ( projectRoot: string, - config: OldCypressConfig, + config: LegacyCypressConfigJson, configFileExists: boolean, ): Promise { const steps: Step[] = [] diff --git a/packages/data-context/test/unit/helper.ts b/packages/data-context/test/unit/helper.ts index 4594561387..52664bc67e 100644 --- a/packages/data-context/test/unit/helper.ts +++ b/packages/data-context/test/unit/helper.ts @@ -9,8 +9,11 @@ import type { BrowserApiShape } from '../../src/sources/BrowserDataSource' import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../src/actions' import { InjectedConfigApi } from '../../src/data' -export function getSystemTestProject (project: typeof e2eProjectDirs[number]) { - return path.join(__dirname, '..', '..', '..', '..', 'system-tests', 'projects', project) +type SystemTestProject = typeof e2eProjectDirs[number] +type SystemTestProjectPath = `${string}/system-tests/projects/${T}` + +export function getSystemTestProject (project: T): SystemTestProjectPath { + return path.join(__dirname, '..', '..', '..', '..', 'system-tests', 'projects', project) as SystemTestProjectPath } export async function scaffoldMigrationProject (project: typeof e2eProjectDirs[number]) { diff --git a/packages/data-context/test/unit/sources/migration/codegen.spec.ts b/packages/data-context/test/unit/sources/migration/codegen.spec.ts index 000e5dd884..7956f42d51 100644 --- a/packages/data-context/test/unit/sources/migration/codegen.spec.ts +++ b/packages/data-context/test/unit/sources/migration/codegen.spec.ts @@ -9,7 +9,6 @@ import { supportFilesForMigration, reduceConfig, renameSupportFilePath, - OldCypressConfig, } from '../../../../src/sources/migration' import { expect } from 'chai' import { MigrationFile } from '../../../../src/sources' @@ -19,7 +18,7 @@ const projectRoot = getSystemTestProject('migration-e2e-defaults') describe('cypress.config.js generation', () => { it('should create a string when passed only a global option', async () => { - const config: OldCypressConfig = { + const config: Partial = { viewportWidth: 300, } @@ -35,7 +34,7 @@ describe('cypress.config.js generation', () => { }) it('should create a string when passed only a e2e options', async () => { - const config: OldCypressConfig = { + const config: Partial = { e2e: { baseUrl: 'localhost:3000', }, @@ -53,7 +52,7 @@ describe('cypress.config.js generation', () => { }) it('should create a string when passed only a component options', async () => { - const config: OldCypressConfig = { + const config: Partial = { component: { retries: 2, }, diff --git a/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts b/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts new file mode 100644 index 0000000000..7b71c22f75 --- /dev/null +++ b/packages/data-context/test/unit/sources/migration/resolveLegacyConfig.spec.ts @@ -0,0 +1,61 @@ +import { expect } from 'chai' +import fs from 'fs-extra' +import path from 'path' +import { processConfigViaLegacyPlugins } from '../../../../src/actions' +import { getSystemTestProject } from '../../helper' + +describe('processConfigViaLegacyPlugins', () => { + it('executes legacy plugins and returns modified config', async () => { + const projectRoot = getSystemTestProject('migration-e2e-plugins-modify-config') + const result = await processConfigViaLegacyPlugins(projectRoot, {}) + + expect(result).to.eql({ + integrationFolder: 'tests/e2e', + testFiles: '**/*.spec.js', + }) + }) + + it('executes legacy plugins and returns without change if pluginsFile returns nothing', async () => { + const projectRoot = getSystemTestProject('migration-e2e-defaults') + const configFile = fs.readJsonSync(path.join(projectRoot, 'cypress.json')) + const result = await processConfigViaLegacyPlugins(projectRoot, configFile) + + expect(result).to.eql(configFile) + }) + + it('works with cypress/plugins/index.ts and export default', async () => { + const projectRoot = getSystemTestProject('migration-e2e-export-default') + const result = await processConfigViaLegacyPlugins(projectRoot, { + retries: 10, + viewportWidth: 8888, + }) + + expect(result).to.eql({ + retries: 10, + viewportWidth: 1111, // mutated in plugins file + }) + }) + + it('catches error', (done) => { + const projectRoot = getSystemTestProject('migration-e2e-legacy-plugins-throws-error') + + processConfigViaLegacyPlugins(projectRoot, {}) + .catch((e) => { + expect(e.originalError.message).to.eq('Uh oh, there was an error!') + done() + }) + }) + + it('handles pluginsFile: false', async () => { + const projectRoot = getSystemTestProject('launchpad') + const result = await processConfigViaLegacyPlugins(projectRoot, { + retries: 10, + viewportWidth: 8888, + }) + + expect(result).to.eql({ + retries: 10, + viewportWidth: 8888, + }) + }) +}) diff --git a/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html b/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html new file mode 100644 index 0000000000..fbeea0d4fc --- /dev/null +++ b/packages/errors/__snapshot-html__/LEGACY_CONFIG_ERROR_DURING_MIGRATION.html @@ -0,0 +1,45 @@ + + + + + + + + + + + +
Your cypress/plugins/index.js at cypress/plugins/index.js threw an error. 
+
+Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to Cypress version 10.0.0.
+
+
+Error: fail whale
+    at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+    at LEGACY_CONFIG_ERROR_DURING_MIGRATION (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)
+
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 547031d4f7..41c20b3b48 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1166,6 +1166,14 @@ export const AllCypressErrors = { ` }, + LEGACY_CONFIG_ERROR_DURING_MIGRATION: (file: string, error: Error) => { + return errTemplate` + Your ${fmt.highlight(file)} at ${fmt.path(`${file}`)} threw an error. ${fmt.stackTrace(error)} + + Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to ${fmt.cypressVersion('10.0.0')}. + ` + }, + LEGACY_CONFIG_FILE: (baseFileName: string, projectRoot: string, legacyConfigFile: string = 'cypress.json') => { return errTemplate` There is both a ${fmt.highlight(baseFileName)} and a ${fmt.highlight(legacyConfigFile)} file at the location below: diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index 9cb011742a..35b791259f 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -297,6 +297,13 @@ describe('visual error templates', () => { // testVisualErrors('CANNOT_RECORD_NO_PROJECT_ID', { testVisualErrors(errorType, { + LEGACY_CONFIG_ERROR_DURING_MIGRATION: () => { + const err = makeErr() + + return { + default: ['cypress/plugins/index.js', err], + } + }, CANNOT_TRASH_ASSETS: () => { const err = makeErr() diff --git a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts index 38789f1268..3ea19692ed 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts @@ -58,7 +58,9 @@ export const e2eProjectDirs = [ 'migration-e2e-export-default', 'migration-e2e-false-plugins-support-file', 'migration-e2e-fully-custom', + 'migration-e2e-legacy-plugins-throws-error', 'migration-e2e-no-plugins-support-file', + 'migration-e2e-plugins-modify-config', 'migration-specs-already-migrated', 'migration-typescript-project', 'module-api', diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 5f2aa68a91..cf5ef00520 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -550,6 +550,7 @@ enum ErrorTypeEnum { INVALID_CYPRESS_INTERNAL_ENV INVALID_REPORTER_NAME INVOKED_BINARY_OUTSIDE_NPM_MODULE + LEGACY_CONFIG_ERROR_DURING_MIGRATION LEGACY_CONFIG_FILE MIGRATION_ALREADY_OCURRED MULTIPLE_SUPPORT_FILES_FOUND diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts index 7879c9a15f..fb01b2a72c 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts @@ -20,15 +20,15 @@ export const MigrationStep = objectType({ t.nonNull.boolean('isCurrentStep', { description: 'This is the current step', resolve: (source, args, ctx) => { - return ctx.migration.step === source.name + return ctx.coreData.migration.step === source.name }, }) t.nonNull.boolean('isCompleted', { description: 'Has the current step been completed', resolve: (source, args, ctx) => { - const indexOfObservedStep = ctx.migration.filteredSteps.indexOf(source.name) - const indexOfCurrentStep = ctx.migration.filteredSteps.indexOf(ctx.migration.step) + const indexOfObservedStep = ctx.coreData.migration.filteredSteps.indexOf(source.name) + const indexOfCurrentStep = ctx.coreData.migration.filteredSteps.indexOf(ctx.coreData.migration.step) return indexOfObservedStep < indexOfCurrentStep }, @@ -37,7 +37,7 @@ export const MigrationStep = objectType({ t.nonNull.int('index', { description: 'Index of the step in the list', resolve: (source, args, ctx) => { - return ctx.migration.filteredSteps.indexOf(source.name) + 1 + return ctx.coreData.migration.filteredSteps.indexOf(source.name) + 1 }, }) }, @@ -148,7 +148,7 @@ export const Migration = objectType({ type: MigrationStep, description: 'Steps filtered with the current context', resolve: (source, args, ctx) => { - return ctx.migration.filteredSteps.map((name) => { + return ctx.coreData.migration.filteredSteps.map((name) => { return { name, } @@ -171,7 +171,7 @@ export const Migration = objectType({ type: ManualMigration, resolve: async (source, args, ctx) => { // avoid starting the watcher when not on this step - if (ctx.migration.step !== 'renameManual') { + if (ctx.coreData.migration.step !== 'renameManual') { return null } @@ -216,7 +216,7 @@ export const Migration = objectType({ t.nonNull.string('configBeforeCode', { description: 'contents of the cypress.json file before conversion', resolve: (source, args, ctx) => { - return ctx.migration.getConfig() + return JSON.stringify(ctx.coreData.migration.legacyConfigForMigration, null, 2) }, }) @@ -240,7 +240,7 @@ export const Migration = objectType({ t.nonNull.boolean('hasCustomIntegrationFolder', { description: 'whether the integration folder is custom or not', resolve: (source, args, ctx) => { - return ctx.migration.hasCustomIntegrationFolder + return ctx.coreData.migration.flags.hasCustomIntegrationFolder } , }) @@ -248,28 +248,28 @@ export const Migration = objectType({ t.nonNull.boolean('hasCustomIntegrationTestFiles', { description: 'whether the testFiles member is custom or not in integration', resolve: (source, args, ctx) => { - return ctx.migration.hasCustomIntegrationTestFiles + return ctx.coreData.migration.flags.hasCustomIntegrationTestFiles }, }) t.nonNull.boolean('hasCustomComponentFolder', { description: 'whether the component folder is custom or not', resolve: (source, args, ctx) => { - return ctx.migration.hasCustomComponentFolder + return ctx.coreData.migration.flags.hasCustomComponentFolder }, }) t.nonNull.boolean('hasCustomComponentTestFiles', { description: 'whether the testFiles member is custom or not in component testing', resolve: (source, args, ctx) => { - return ctx.migration.hasCustomComponentTestFiles + return ctx.coreData.migration.flags.hasCustomComponentTestFiles }, }) t.nonNull.boolean('hasComponentTesting', { description: 'whether component testing is set up in the migrated config or not', resolve: (source, args, ctx) => { - return ctx.migration.hasComponentTesting + return ctx.coreData.migration.flags.hasComponentTesting }, }) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 3fed1385c8..bae0d3a8bd 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -475,7 +475,7 @@ export const mutation = mutationType({ description: 'Initialize the migration wizard to the first step', type: Query, resolve: async (_, args, ctx) => { - await ctx.actions.migration.initialize() + await ctx.lifecycleManager._pendingMigrationInitialize?.promise return {} }, diff --git a/packages/launchpad/cypress/e2e/migration.cy.ts b/packages/launchpad/cypress/e2e/migration.cy.ts index 19cea14596..2cf85894ba 100644 --- a/packages/launchpad/cypress/e2e/migration.cy.ts +++ b/packages/launchpad/cypress/e2e/migration.cy.ts @@ -18,10 +18,14 @@ Cypress.Commands.add('waitForWizard', () => { return cy.get('[data-cy="migration-wizard"]') }) -function startMigrationFor (project: typeof e2eProjectDirs[number], argv?: string[]) { +function scaffoldAndVisitLaunchpad (project: typeof e2eProjectDirs[number], argv?: string[]) { cy.scaffoldProject(project) cy.openProject(project, argv) cy.visitLaunchpad() +} + +function startMigrationFor (project: typeof e2eProjectDirs[number], argv?: string[]) { + scaffoldAndVisitLaunchpad(project, argv) cy.waitForWizard() } @@ -523,6 +527,21 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo checkOutcome() }) + it('completes journey for migration-e2e-plugins-modify-config', () => { + startMigrationFor('migration-e2e-plugins-modify-config') + // No rename, integrationFolder and testFiles are custom (via plugins) + cy.get(renameAutoStep).should('not.exist') + // no CT + cy.get(renameManualStep).should('not.exist') + cy.get(renameSupportStep).should('exist') + cy.get(setupComponentStep).should('not.exist') + cy.get(configFileStep).should('exist') + + renameSupport() + migrateAndVerifyConfig() + checkOutcome() + }) + it('completes journey for migration-e2e-no-plugins-support-file', () => { startMigrationFor('migration-e2e-no-plugins-support-file') // defaults, rename all the things @@ -813,6 +832,22 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo migrateAndVerifyConfig() }) }) + + it('completes journey for migration-e2e-legacy-plugins-throws-error', () => { + scaffoldAndVisitLaunchpad('migration-e2e-legacy-plugins-throws-error') + // no steps are shown - we show the error that surfaced when executing pluginsFile. + cy.get(renameAutoStep).should('not.exist') + cy.get(renameManualStep).should('not.exist') + cy.get(renameSupportStep).should('not.exist') + cy.get(setupComponentStep).should('not.exist') + cy.get(configFileStep).should('not.exist') + + cy.contains('Error Loading Config') + // correct location of error + cy.get('[data-testid="error-code-frame"]').contains(`cypress/plugins/index.js:2:9`) + // correct error from pluginsFile + cy.contains(`throw Error('Uh oh, there was an error!')`) + }) }) // TODO: toLaunchpad emitter not working in Cypress in Cypress. @@ -1095,18 +1130,14 @@ describe('Migrate custom config files', () => { }) it('shows error for migration-custom-config-file-migration-already-ocurred', () => { - cy.scaffoldProject('migration-custom-config-file-migration-already-ocurred') - cy.openProject('migration-custom-config-file-migration-already-ocurred', ['--config-file', 'customConfig.json']) - cy.visitLaunchpad() + scaffoldAndVisitLaunchpad('migration-custom-config-file-migration-already-ocurred', ['--config-file', 'customConfig.json']) cy.contains('You are attempting to use Cypress with an older config file: customConfig.json') cy.contains('When you upgraded to Cypress v10.0 the config file was updated and moved to a new location: customConfig.config.js') }) it('shows error for migration-custom-config-file-with-existing-v10-config-file', () => { - cy.scaffoldProject('migration-custom-config-file-with-existing-v10-config-file') - cy.openProject('migration-custom-config-file-with-existing-v10-config-file', ['--config-file', 'customConfig.json']) - cy.visitLaunchpad() + scaffoldAndVisitLaunchpad('migration-custom-config-file-with-existing-v10-config-file', ['--config-file', 'customConfig.json']) cy.contains('There is both a customConfig.config.js and a customConfig.json file at the location below:') cy.contains('ypress no longer supports customConfig.json, please remove it from your project.') diff --git a/packages/server/lib/plugins/child/run_require_async_child.js b/packages/server/lib/plugins/child/run_require_async_child.js index db7f6f3ee5..0ef924639b 100644 --- a/packages/server/lib/plugins/child/run_require_async_child.js +++ b/packages/server/lib/plugins/child/run_require_async_child.js @@ -8,14 +8,14 @@ const { RunPlugins } = require('./run_plugins') let tsRegistered = false /** - * Executes and returns the passed `configFile` file in the ipc `loadConfig` event + * Executes and returns the passed `file` (usually `configFile`) file in the ipc `loadConfig` event * @param {*} ipc Inter Process Communication protocol - * @param {*} configFile the file we are trying to load + * @param {*} file the file we are trying to load * @param {*} projectRoot the root of the typescript project (useful mainly for tsnode) * @returns */ -function run (ipc, configFile, projectRoot) { - debug('configFile:', configFile) +function run (ipc, file, projectRoot) { + debug('configFile:', file) debug('projectRoot:', projectRoot) if (!projectRoot) { throw new Error('Unexpected: projectRoot should be a string') @@ -23,7 +23,7 @@ function run (ipc, configFile, projectRoot) { if (!tsRegistered) { debug('register typescript for required file') - tsNodeUtil.register(projectRoot, configFile) + tsNodeUtil.register(projectRoot, file) // ensure typescript is only registered once tsRegistered = true @@ -55,7 +55,7 @@ function run (ipc, configFile, projectRoot) { const isValidSetupNodeEvents = (config, testingType) => { if (config[testingType] && config[testingType].setupNodeEvents && typeof config[testingType].setupNodeEvents !== 'function') { ipc.send('setupTestingType:error', util.serializeError( - require('@packages/errors').getError('SETUP_NODE_EVENTS_IS_NOT_FUNCTION', configFile, testingType, config[testingType].setupNodeEvents), + require('@packages/errors').getError('SETUP_NODE_EVENTS_IS_NOT_FUNCTION', file, testingType, config[testingType].setupNodeEvents), )) return false @@ -72,16 +72,73 @@ function run (ipc, configFile, projectRoot) { } ipc.send('setupTestingType:error', util.serializeError( - require('@packages/errors').getError('CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION', configFile, config), + require('@packages/errors').getError('CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION', file, config), )) return false } + ipc.on('loadLegacyPlugins', async (legacyConfig) => { + try { + let legacyPlugins = require(file) + + if (legacyPlugins && typeof legacyPlugins.default === 'function') { + legacyPlugins = legacyPlugins.default + } + + // invalid or empty plugins file + if (typeof legacyPlugins !== 'function') { + ipc.send('loadLegacyPlugins:reply', legacyConfig) + + return + } + + // we do not want to execute any tasks - the purpose + // of this is to get any modified config returned + // by plugins. + const noop = () => {} + const legacyPluginsConfig = await legacyPlugins(noop, legacyConfig) + + // pluginsFile did not return the config - this is allowed, although + // we recommend returning it in our docs. + if (!legacyPluginsConfig) { + ipc.send('loadLegacyPlugins:reply', legacyConfig) + + return + } + + // match merging strategy from 9.x + const mergedLegacyConfig = { + ...legacyConfig, + ...legacyPluginsConfig, + } + + if (legacyConfig.e2e || legacyPluginsConfig.e2e) { + mergedLegacyConfig.e2e = { + ...(legacyConfig.e2e || {}), + ...(legacyPluginsConfig.e2e || {}), + } + } + + if (legacyConfig.component || legacyPluginsConfig.component) { + mergedLegacyConfig.component = { + ...(legacyConfig.component || {}), + ...(legacyPluginsConfig.component || {}), + } + } + + ipc.send('loadLegacyPlugins:reply', mergedLegacyConfig) + } catch (e) { + ipc.send('loadLegacyPlugins:error', util.serializeError( + require('@packages/errors').getError('LEGACY_CONFIG_ERROR_DURING_MIGRATION', file, e), + )) + } + }) + ipc.on('loadConfig', () => { try { - debug('try loading', configFile) - const exp = require(configFile) + debug('try loading', file) + const exp = require(file) const result = exp.default || exp @@ -102,7 +159,7 @@ function run (ipc, configFile, projectRoot) { debug(`setupTestingType %s %o`, testingType, options) - const runPlugins = new RunPlugins(ipc, projectRoot, configFile) + const runPlugins = new RunPlugins(ipc, projectRoot, file) if (!isValidSetupNodeEvents(result, testingType)) { return @@ -134,7 +191,7 @@ function run (ipc, configFile, projectRoot) { } }) - debug('loaded config from %s %o', configFile, result) + debug('loaded config from %s %o', file, result) } catch (err) { if (err.name === 'TSError') { // because of this https://github.com/TypeStrong/ts-node/issues/1418 @@ -153,7 +210,7 @@ function run (ipc, configFile, projectRoot) { } ipc.send('loadConfig:error', util.serializeError( - require('@packages/errors').getError('CONFIG_FILE_REQUIRE_ERROR', configFile, err), + require('@packages/errors').getError('CONFIG_FILE_REQUIRE_ERROR', file, err), )) } }) diff --git a/packages/types/src/constants.ts b/packages/types/src/constants.ts index 00cbec1370..081d4e16d1 100644 --- a/packages/types/src/constants.ts +++ b/packages/types/src/constants.ts @@ -17,4 +17,6 @@ export type CodeLanguage = typeof CODE_LANGUAGES[number] export const MIGRATION_STEPS = ['renameAuto', 'renameManual', 'renameSupport', 'configFile', 'setupComponent'] as const +export type MigrationStep = typeof MIGRATION_STEPS[number] + export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'] as const diff --git a/system-tests/projects/migration-e2e-component-default-everything/expected-cypress.config.js b/system-tests/projects/migration-e2e-component-default-everything/expected-cypress.config.js index 07b02fe9e7..b87df85eb3 100644 --- a/system-tests/projects/migration-e2e-component-default-everything/expected-cypress.config.js +++ b/system-tests/projects/migration-e2e-component-default-everything/expected-cypress.config.js @@ -1,6 +1,7 @@ const { defineConfig } = require('cypress') module.exports = defineConfig({ + test: 'value', e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. diff --git a/system-tests/projects/migration-e2e-component-default-test-files/expected-cypress.config.js b/system-tests/projects/migration-e2e-component-default-test-files/expected-cypress.config.js index 4965cf5306..1447fc978d 100644 --- a/system-tests/projects/migration-e2e-component-default-test-files/expected-cypress.config.js +++ b/system-tests/projects/migration-e2e-component-default-test-files/expected-cypress.config.js @@ -1,6 +1,7 @@ const { defineConfig } = require('cypress') module.exports = defineConfig({ + test: 'value', e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. diff --git a/system-tests/projects/migration-e2e-component-default-with-types/expected-cypress.config.js b/system-tests/projects/migration-e2e-component-default-with-types/expected-cypress.config.js index 07b02fe9e7..b87df85eb3 100644 --- a/system-tests/projects/migration-e2e-component-default-with-types/expected-cypress.config.js +++ b/system-tests/projects/migration-e2e-component-default-with-types/expected-cypress.config.js @@ -1,6 +1,7 @@ const { defineConfig } = require('cypress') module.exports = defineConfig({ + test: 'value', e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. diff --git a/system-tests/projects/migration-e2e-component-with-json-files/cypress/plugins/index.js b/system-tests/projects/migration-e2e-component-with-json-files/cypress/plugins/index.js index af7dd141fc..e52f8fa071 100644 --- a/system-tests/projects/migration-e2e-component-with-json-files/cypress/plugins/index.js +++ b/system-tests/projects/migration-e2e-component-with-json-files/cypress/plugins/index.js @@ -1,5 +1,3 @@ module.exports = (on, config) => { - return { - test: 'value', - } + return {} } diff --git a/system-tests/projects/migration-e2e-defaults/cypress/plugins/index.js b/system-tests/projects/migration-e2e-defaults/cypress/plugins/index.js index e69de29bb2..a61659e3ba 100644 --- a/system-tests/projects/migration-e2e-defaults/cypress/plugins/index.js +++ b/system-tests/projects/migration-e2e-defaults/cypress/plugins/index.js @@ -0,0 +1,3 @@ +module.exports = (on, config) => { + +} diff --git a/system-tests/projects/migration-e2e-export-default/cypress/plugins/index.ts b/system-tests/projects/migration-e2e-export-default/cypress/plugins/index.ts index e4ee7dbcaa..6bfc40181b 100644 --- a/system-tests/projects/migration-e2e-export-default/cypress/plugins/index.ts +++ b/system-tests/projects/migration-e2e-export-default/cypress/plugins/index.ts @@ -1,6 +1,12 @@ -export default function (on, config) { +export default async function (on, config) { // eslint-disable-next-line const foo: number = 123 // to make sure the parser handles TS + const asyncViewport = () => Promise.resolve(1111) + + // make sure we consider that config can be mutated + // asynchronously in plugins + config.viewportWidth = await asyncViewport() + return config } diff --git a/system-tests/projects/migration-e2e-export-default/expected-cypress.config.ts b/system-tests/projects/migration-e2e-export-default/expected-cypress.config.ts index ab4efd76f3..2fce3cb264 100644 --- a/system-tests/projects/migration-e2e-export-default/expected-cypress.config.ts +++ b/system-tests/projects/migration-e2e-export-default/expected-cypress.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'cypress' export default defineConfig({ + viewportWidth: 1111, e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/README.md b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/README.md new file mode 100644 index 0000000000..05ce74eead --- /dev/null +++ b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/README.md @@ -0,0 +1,27 @@ +## Migration E2E Legact Plugins Throws Error + +An e2e project where `cypress/plugins/index.js` throws an error during migration. + +The following migration steps will be used during this migration: + +- [ ] automatic file rename +- [ ] manual file rename +- [ ] rename support +- [ ] update config file +- [ ] setup component testing + +## Automatic Migration + +Not shown - we don't get this far, an error is throw. + +## Manual Files + +Not shown - we don't get this far, an error is throw. + +## Rename supportFile + +Not shown - we don't get this far, an error is throw. + +## Update Config + +Not shown - we don't get this far, an error is throw. diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress.json b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress.json @@ -0,0 +1 @@ +{} diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress/plugins/index.js b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress/plugins/index.js new file mode 100644 index 0000000000..e31af1820b --- /dev/null +++ b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress/plugins/index.js @@ -0,0 +1,3 @@ +module.exports = (on, config) => { + throw Error('Uh oh, there was an error!') +} diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress/support/index.js b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/cypress/support/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/expected-cypress.config.js b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/expected-cypress.config.js new file mode 100644 index 0000000000..948ae1c2db --- /dev/null +++ b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/expected-cypress.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents (on, config) { + return require('./cypress/plugins/index.js')(on, config) + }, + specPattern: 'tests/e2e/**/*.spec.js', + }, +}) diff --git a/system-tests/projects/migration-e2e-legacy-plugins-throws-error/tests/e2e/foo.spec.js b/system-tests/projects/migration-e2e-legacy-plugins-throws-error/tests/e2e/foo.spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/README.md b/system-tests/projects/migration-e2e-plugins-modify-config/README.md new file mode 100644 index 0000000000..2957d82e7d --- /dev/null +++ b/system-tests/projects/migration-e2e-plugins-modify-config/README.md @@ -0,0 +1,31 @@ +## Migration E2E Plugins Modify Config + +An e2e project where `cypress/plugins/index.js` modifies the `config`, specifically `integrationFolder` and `testFiles`. + +The following migration steps will be used during this migration: + +- [ ] automatic file rename +- [ ] manual file rename +- [x] rename support +- [x] update config file +- [ ] setup component testing + +## Automatic Migration + +We do not show this step because both `integrationFolder` and `testFiles` are custom (via plugins). + +## Manual Files + +This step is not used. + +## Rename supportFile + +The project has a default support file, `cypress/support/index.js`. We can rename it for them to `cypress/support/e2e.js`. + +| Before | After| +|---|---| +| `cypress/support/index.js` | `cypress/support/e2e.js` | + +## Update Config + +The expected output is in [`expected-cypress.config.js`](./expected-cypress.config.js). diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/cypress.json b/system-tests/projects/migration-e2e-plugins-modify-config/cypress.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/system-tests/projects/migration-e2e-plugins-modify-config/cypress.json @@ -0,0 +1 @@ +{} diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/cypress/plugins/index.js b/system-tests/projects/migration-e2e-plugins-modify-config/cypress/plugins/index.js new file mode 100644 index 0000000000..e8c01a1aa6 --- /dev/null +++ b/system-tests/projects/migration-e2e-plugins-modify-config/cypress/plugins/index.js @@ -0,0 +1,6 @@ +module.exports = (on, config) => { + config.integrationFolder = 'tests/e2e' + config.testFiles = '**/*.spec.js' + + return config +} diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/cypress/support/index.js b/system-tests/projects/migration-e2e-plugins-modify-config/cypress/support/index.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/expected-cypress.config.js b/system-tests/projects/migration-e2e-plugins-modify-config/expected-cypress.config.js new file mode 100644 index 0000000000..948ae1c2db --- /dev/null +++ b/system-tests/projects/migration-e2e-plugins-modify-config/expected-cypress.config.js @@ -0,0 +1,12 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents (on, config) { + return require('./cypress/plugins/index.js')(on, config) + }, + specPattern: 'tests/e2e/**/*.spec.js', + }, +}) diff --git a/system-tests/projects/migration-e2e-plugins-modify-config/tests/e2e/foo.spec.js b/system-tests/projects/migration-e2e-plugins-modify-config/tests/e2e/foo.spec.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/system-tests/projects/migration/expected-cypress.config.js b/system-tests/projects/migration/expected-cypress.config.js index ec68096ee2..d08d0bbb40 100644 --- a/system-tests/projects/migration/expected-cypress.config.js +++ b/system-tests/projects/migration/expected-cypress.config.js @@ -4,6 +4,7 @@ module.exports = defineConfig({ retries: 2, defaultCommandTimeout: 5000, fixturesFolder: false, + test: 'value', e2e: { // We've imported your old cypress plugins here. // You may want to clean this up later by importing these.