fix(launchpad): correctly migrate projects settings config in plugins (#20509)

Co-authored-by: Barthélémy Ledoux <bart@cypress.io>
This commit is contained in:
Lachlan Miller
2022-03-12 05:04:06 +10:00
committed by GitHub
parent 00c3c1b2e0
commit 742f261a1b
42 changed files with 684 additions and 310 deletions

View File

@@ -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<LegacyCypressConfigJson> {
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
})
}
}

View File

@@ -181,7 +181,7 @@ export class ProjectActions {
}
if (args.open) {
await this.setCurrentProject(projectRoot)
this.setCurrentProject(projectRoot).catch(this.ctx.onError)
}
}

View File

@@ -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)
}
}

View File

@@ -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<void>
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) {

View File

@@ -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> = 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<AllModeOptions> = {}): 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,

View File

@@ -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<LegacyCypressConfigJson, 'component' | 'e2e'>
e2e: Omit<LegacyCypressConfigJson, 'component' | 'e2e'>
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<OldCypressConfig> | 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<OldCypressConfig> {
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<OldCypressConfig>
}
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'}`)
}

View File

@@ -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<GetSpecs> {
export async function getSpecs (projectRoot: string, config: LegacyCypressConfigJson): Promise<GetSpecs> {
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) => {

View File

@@ -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<OldCypressConfig, 'component' | 'e2e'>
e2e?: Omit<OldCypressConfig, 'component' | 'e2e'>
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<string> {
return cfg.pluginsFile ? cfg.pluginsFile : await tryGetDefaultLegacyPluginsFile(projectRoot) || ''
async function getPluginRelativePath (cfg: LegacyCypressConfigJson, projectRoot: string): Promise<string | undefined> {
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<string, unknown>) {
return JSON.stringify(obj, null, 2).replace(/^[{]|[}]$/g, '') // remove opening and closing {}
}
function createE2ETemplate (pluginPath: string, createConfigOptions: CreateConfigOptions, options: Record<string, unknown>) {
if (!createConfigOptions.hasPluginsFile) {
function createE2ETemplate (pluginPath: string | undefined, createConfigOptions: CreateConfigOptions, options: Record<string, unknown>) {
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

View File

@@ -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<Step[]> {
const steps: Step[] = []

View File

@@ -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<T extends SystemTestProject> = `${string}/system-tests/projects/${T}`
export function getSystemTestProject<T extends typeof e2eProjectDirs[number]> (project: T): SystemTestProjectPath<T> {
return path.join(__dirname, '..', '..', '..', '..', 'system-tests', 'projects', project) as SystemTestProjectPath<T>
}
export async function scaffoldMigrationProject (project: typeof e2eProjectDirs[number]) {

View File

@@ -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<Cypress.Config> = {
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<Cypress.Config> = {
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<Cypress.Config> = {
component: {
retries: 2,
},

View File

@@ -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,
})
})
})

View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
<style>
body {
font-family: "Courier Prime", Courier, monospace;
padding: 0 1em;
line-height: 1.4;
color: #eee;
background-color: #111;
}
pre {
padding: 0 0;
margin: 0 0;
font-family: "Courier Prime", Courier, monospace;
}
body {
margin: 5px;
padding: 0;
overflow: hidden;
}
pre {
white-space: pre-wrap;
word-break: break-word;
-webkit-font-smoothing: antialiased;
}
</style>
</head>
<body><pre><span style="color:#e05561">Your <span style="color:#e5e510">cypress/plugins/index.js<span style="color:#e05561"> at <span style="color:#4ec4ff">cypress/plugins/index.js<span style="color:#e05561"> threw an error. <span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#e05561">Please ensure your pluginsFile is valid and relaunch the migration tool to migrate to Cypress version 10.0.0.<span style="color:#e6e6e6">
<span style="color:#e05561"><span style="color:#e6e6e6">
<span style="color:#c062de"><span style="color:#e6e6e6">
<span style="color:#c062de">Error: fail whale<span style="color:#e6e6e6">
<span style="color:#c062de"> at makeErr (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6">
<span style="color:#c062de"> at LEGACY_CONFIG_ERROR_DURING_MIGRATION (cypress/packages/errors/test/unit/visualSnapshotErrors_spec.ts)<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
</pre></body></html>

View File

@@ -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:

View File

@@ -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()

View File

@@ -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',

View File

@@ -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

View File

@@ -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
},
})

View File

@@ -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 {}
},

View File

@@ -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.')

View File

@@ -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),
))
}
})

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,5 +1,3 @@
module.exports = (on, config) => {
return {
test: 'value',
}
return {}
}

View File

@@ -0,0 +1,3 @@
module.exports = (on, config) => {
}

View File

@@ -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
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -0,0 +1,3 @@
module.exports = (on, config) => {
throw Error('Uh oh, there was an error!')
}

View File

@@ -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',
},
})

View File

@@ -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).

View File

@@ -0,0 +1,6 @@
module.exports = (on, config) => {
config.integrationFolder = 'tests/e2e'
config.testFiles = '**/*.spec.js'
return config
}

View File

@@ -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',
},
})

View File

@@ -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.