From bdc7c01d62fa2f3ed578be149a12cb1c8cdbc066 Mon Sep 17 00:00:00 2001 From: Cesar Date: Thu, 20 Jan 2022 14:43:09 -0700 Subject: [PATCH] feat: convert cypress.json to cypress.config.js (#19748) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Barthélémy Ledoux Co-authored-by: ElevateBart --- .../__snapshots__/config-generator.spec.ts.js | 72 ++++++++ packages/data-context/src/DataActions.ts | 6 + packages/data-context/src/DataContext.ts | 6 + .../src/actions/MigrationActions.ts | 19 +++ packages/data-context/src/actions/index.ts | 1 + .../src/sources/FileDataSource.ts | 10 ++ .../src/sources/MigrationDataSource.ts | 64 ++++++++ packages/data-context/src/sources/index.ts | 1 + packages/data-context/src/util/index.ts | 1 + packages/data-context/src/util/migration.ts | 80 +++++++++ .../test/unit/config-generator.spec.ts | 75 +++++++++ .../cypress/e2e/support/e2eProjectDirs.ts | 1 + .../support/mock-graphql/stubgql-Migration.ts | 2 + packages/graphql/schemas/schema.graphql | 9 + .../schemaTypes/objectTypes/gql-Migration.ts | 24 ++- .../schemaTypes/objectTypes/gql-Mutation.ts | 14 ++ .../launchpad/cypress/e2e/migration.cy.ts | 41 +++++ packages/launchpad/src/Main.vue | 6 +- .../src/migration/MigrationWizard.cy.tsx | 51 ++++-- .../src/migration/MigrationWizard.vue | 155 ++++++++++-------- system-tests/projects/migration/cypress.json | 18 ++ .../migration/cypress/plugins/index.js | 5 + 22 files changed, 570 insertions(+), 91 deletions(-) create mode 100644 packages/data-context/__snapshots__/config-generator.spec.ts.js create mode 100644 packages/data-context/src/actions/MigrationActions.ts create mode 100644 packages/data-context/src/sources/MigrationDataSource.ts create mode 100644 packages/data-context/src/util/migration.ts create mode 100644 packages/data-context/test/unit/config-generator.spec.ts create mode 100644 packages/launchpad/cypress/e2e/migration.cy.ts create mode 100644 system-tests/projects/migration/cypress.json create mode 100644 system-tests/projects/migration/cypress/plugins/index.js diff --git a/packages/data-context/__snapshots__/config-generator.spec.ts.js b/packages/data-context/__snapshots__/config-generator.spec.ts.js new file mode 100644 index 0000000000..2b61e80f89 --- /dev/null +++ b/packages/data-context/__snapshots__/config-generator.spec.ts.js @@ -0,0 +1,72 @@ +exports['migration utils should create a string when passed only a global option 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + visualViewport: 300, +}) +` + +exports['migration utils should create a string when passed an empty object 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + +}) +` + +exports['migration utils should create a string when passed only a e2e options 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + + e2e: { + setupNodeEvents(on, config) { + return require('/cypress/plugins/index.js') + }, + baseUrl: 'localhost:3000' + }, +}) +` + +exports['migration utils should create a string for a config with global, component, and e2e options 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + visualViewport: 300, + e2e: { + setupNodeEvents(on, config) { + return require('/cypress/plugins/index.js') + }, + baseUrl: 'localhost:300', + retries: 2 + }, + component: { + setupNodeEvents(on, config) { + return require('/cypress/plugins/index.js') + }, + retries: 1 + }, +}) +` + +exports['migration utils should create a string when passed only a component options 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + + component: { + setupNodeEvents(on, config) { + return require('/cypress/plugins/index.js') + }, + retries: 2 + }, +}) +` + +exports['migration utils should exclude fields that are no longer valid 1'] = ` +const { defineConfig } = require('cypress') + +module.export = defineConfig({ + +}) +` diff --git a/packages/data-context/src/DataActions.ts b/packages/data-context/src/DataActions.ts index fa3e5f588f..31e536cd69 100644 --- a/packages/data-context/src/DataActions.ts +++ b/packages/data-context/src/DataActions.ts @@ -6,6 +6,7 @@ import { FileActions, ProjectActions, WizardActions, + MigrationActions, } from './actions' import { AuthActions } from './actions/AuthActions' import { DevActions } from './actions/DevActions' @@ -53,4 +54,9 @@ export class DataActions { get electron () { return new ElectronActions(this.ctx) } + + @cached + get migration () { + return new MigrationActions(this.ctx) + } } diff --git a/packages/data-context/src/DataContext.ts b/packages/data-context/src/DataContext.ts index f5b4c45054..77cb566b77 100644 --- a/packages/data-context/src/DataContext.ts +++ b/packages/data-context/src/DataContext.ts @@ -28,6 +28,7 @@ import { HtmlDataSource, UtilDataSource, BrowserApiShape, + MigrationDataSource, } from './sources/' import { cached } from './util/cached' import type { GraphQLSchema } from 'graphql' @@ -225,6 +226,11 @@ export class DataContext { return new UtilDataSource(this) } + @cached + get migration () { + return new MigrationDataSource(this) + } + get projectsList () { return this.coreData.app.projects } diff --git a/packages/data-context/src/actions/MigrationActions.ts b/packages/data-context/src/actions/MigrationActions.ts new file mode 100644 index 0000000000..7f00fea176 --- /dev/null +++ b/packages/data-context/src/actions/MigrationActions.ts @@ -0,0 +1,19 @@ +import type { DataContext } from '..' + +export class MigrationActions { + constructor (private ctx: DataContext) { } + + async createConfigFile () { + const config = await this.ctx.migration.createConfigString() + + await this.ctx.actions.file.writeFileInProject('cypress.config.js', config).catch((error) => { + throw error + }) + + this.ctx.lifecycleManager.refreshMetaState() + + await this.ctx.actions.file.removeFileInProject('cypress.json').catch((error) => { + throw error + }) + } +} diff --git a/packages/data-context/src/actions/index.ts b/packages/data-context/src/actions/index.ts index f591e7e451..8ab30cc692 100644 --- a/packages/data-context/src/actions/index.ts +++ b/packages/data-context/src/actions/index.ts @@ -8,5 +8,6 @@ export * from './DevActions' export * from './ElectronActions' export * from './FileActions' export * from './LocalSettingsActions' +export * from './MigrationActions' export * from './ProjectActions' export * from './WizardActions' diff --git a/packages/data-context/src/sources/FileDataSource.ts b/packages/data-context/src/sources/FileDataSource.ts index 03d99aedd0..1c8b85d008 100644 --- a/packages/data-context/src/sources/FileDataSource.ts +++ b/packages/data-context/src/sources/FileDataSource.ts @@ -74,6 +74,16 @@ export class FileDataSource { } } + isValidJsFile (absolutePath: string) { + try { + require(absolutePath) + + return true + } catch { + return false + } + } + private trackFile () { // this.watchedFilePaths.clear() // this.fileLoader.clear() diff --git a/packages/data-context/src/sources/MigrationDataSource.ts b/packages/data-context/src/sources/MigrationDataSource.ts new file mode 100644 index 0000000000..b376ae1d62 --- /dev/null +++ b/packages/data-context/src/sources/MigrationDataSource.ts @@ -0,0 +1,64 @@ +import path from 'path' +import type { DataContext } from '..' +import { createConfigString } from '../util/migration' + +export class MigrationDataSource { + private _config: Cypress.ConfigOptions | null = null + constructor (private ctx: DataContext) { } + + async getConfig () { + const config = await this.parseCypressConfig() + + return JSON.stringify(config, null, 2) + } + + async createConfigString () { + const config = await this.parseCypressConfig() + + return createConfigString(config) + } + + async getIntegrationFolder () { + const config = await this.parseCypressConfig() + + if (config.e2e?.integrationFolder) { + return config.e2e.integrationFolder + } + + if (config.integrationFolder) { + return config.integrationFolder + } + + return 'cypress/integration' + } + + async getComponentFolder () { + const config = await this.parseCypressConfig() + + if (config.component?.componentFolder) { + return config.component.componentFolder + } + + if (config.componentFolder) { + return config.componentFolder + } + + return 'cypress/component' + } + + private async parseCypressConfig (): Promise { + if (this._config) { + return this._config + } + + if (this.ctx.lifecycleManager.metaState.hasLegacyCypressJson) { + const cfgPath = path.join(this.ctx.lifecycleManager?.projectRoot, 'cypress.json') + + this._config = this.ctx.file.readJsonFile(cfgPath) as Cypress.ConfigOptions + + return this._config + } + + return {} + } +} diff --git a/packages/data-context/src/sources/index.ts b/packages/data-context/src/sources/index.ts index 5b4c1211f3..b5ca9b7a59 100644 --- a/packages/data-context/src/sources/index.ts +++ b/packages/data-context/src/sources/index.ts @@ -8,6 +8,7 @@ export * from './FileDataSource' export * from './GitDataSource' export * from './GraphQLDataSource' export * from './HtmlDataSource' +export * from './MigrationDataSource' export * from './ProjectDataSource' export * from './SettingsDataSource' export * from './StorybookDataSource' diff --git a/packages/data-context/src/util/index.ts b/packages/data-context/src/util/index.ts index c6945ae47f..b86833842c 100644 --- a/packages/data-context/src/util/index.ts +++ b/packages/data-context/src/util/index.ts @@ -6,4 +6,5 @@ export * from './cached' export * from './config-file-updater' export * from './config-options' export * from './file' +export * from './migration' export * from './urqlCacheKeys' diff --git a/packages/data-context/src/util/migration.ts b/packages/data-context/src/util/migration.ts new file mode 100644 index 0000000000..b25ba3cb0b --- /dev/null +++ b/packages/data-context/src/util/migration.ts @@ -0,0 +1,80 @@ +import path from 'path' + +type ConfigOptions = { + global: Record + e2e: Record + component: Record +} + +export async function createConfigString (cfg: Partial) { + return createCypressConfigJs(reduceConfig(cfg), getPluginRelativePath(cfg)) +} + +function getPluginRelativePath (cfg: Partial) { + const DEFAULT_PLUGIN_PATH = path.normalize('/cypress/plugins/index.js') + + return cfg.pluginsFile ? cfg.pluginsFile : DEFAULT_PLUGIN_PATH +} + +function reduceConfig (cfg: Partial) { + const excludedFields = ['pluginsFile', '$schema', 'componentFolder'] + + return Object.entries(cfg).reduce((acc, [key, val]) => { + if (excludedFields.includes(key)) { + return acc + } + + if (key === 'e2e' || key === 'component') { + const value = val as Record + + return { ...acc, [key]: { ...acc[key], ...value } } + } + + if (key === 'testFiles') { + return { + ...acc, + e2e: { ...acc.e2e, specPattern: val }, + component: { ...acc.component, specPattern: val }, + } + } + + if (key === 'baseUrl') { + return { + ...acc, + e2e: { ...acc.e2e, [key]: val }, + } + } + + return { ...acc, global: { ...acc.global, [key]: val } } + }, { global: {}, e2e: {}, component: {} }) +} + +function createCypressConfigJs (config: ConfigOptions, pluginPath: string) { + const globalString = Object.keys(config.global).length > 0 ? `${formatObjectForConfig(config.global, 2)},` : '' + const componentString = Object.keys(config.component).length > 0 ? createTestingTypeTemplate('component', pluginPath, config.component) : '' + const e2eString = Object.keys(config.e2e).length > 0 ? createTestingTypeTemplate('e2e', pluginPath, config.e2e) : '' + + return `const { defineConfig } = require('cypress') + +module.export = defineConfig({ + ${globalString}${e2eString}${componentString} +})` +} + +function formatObjectForConfig (obj: Record, spaces: number) { + return JSON.stringify(obj, null, spaces) + .replace(/"([^"]+)":/g, '$1:') // remove quotes from fields + .replace(/^[{]|[}]$/g, '') // remove opening and closing {} + .replace(/"/g, '\'') // single quotes + .trim() +} + +function createTestingTypeTemplate (testingType: 'e2e' | 'component', pluginPath: string, options: Record) { + return ` + ${testingType}: { + setupNodeEvents(on, config) { + return require('${pluginPath}') + }, + ${formatObjectForConfig(options, 4)} + },` +} diff --git a/packages/data-context/test/unit/config-generator.spec.ts b/packages/data-context/test/unit/config-generator.spec.ts new file mode 100644 index 0000000000..3fd090eb69 --- /dev/null +++ b/packages/data-context/test/unit/config-generator.spec.ts @@ -0,0 +1,75 @@ +import snapshot from 'snap-shot-it' +import { createConfigString } from '../../src/util/migration' + +describe('migration utils', () => { + it('should create a string when passed only a global option', async () => { + const config = { + visualViewport: 300, + } + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) + + it('should create a string when passed only a e2e options', async () => { + const config = { + e2e: { + baseUrl: 'localhost:3000', + }, + } + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) + + it('should create a string when passed only a component options', async () => { + const config = { + component: { + retries: 2, + }, + } + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) + + it('should create a string for a config with global, component, and e2e options', async () => { + const config = { + visualViewport: 300, + baseUrl: 'localhost:300', + e2e: { + retries: 2, + }, + component: { + retries: 1, + }, + } + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) + + it('should create a string when passed an empty object', async () => { + const config = {} + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) + + it('should exclude fields that are no longer valid', async () => { + const config = { + '$schema': 'http://someschema.com', + pluginsFile: 'path/to/plugin/file', + componentFolder: 'path/to/component/folder', + } + + const generatedConfig = await createConfigString(config) + + snapshot(generatedConfig) + }) +}) diff --git a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts index aadfbde3cf..e1e7aab586 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts @@ -29,6 +29,7 @@ export const e2eProjectDirs = [ 'kill-child-process', 'launchpad', 'max-listeners', + 'migration', 'multiple-task-registrations', 'multiples-config-files-with-json', 'no-scaffolding', diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts index 91f54ebee4..ccc36a93fe 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Migration.ts @@ -47,4 +47,6 @@ export const stubMigration: MaybeResolver = { } }, })`, + integrationFolder: 'cypress/integration', + componentFolder: 'cypress/component', } diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index ea8a39341e..11af50dba5 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -543,12 +543,18 @@ type LocalSettingsPreferences { """Contains all data related to the 9.X to 10.0 migration UI""" type Migration { + """the component folder path used to store components tests""" + componentFolder: String! + """contents of the cypress.json file after conversion""" configAfterCode: String! """contents of the cypress.json file before conversion""" configBeforeCode: String! + """the integration folder path used to store e2e tests""" + integrationFolder: String! + """List of files needing manual conversion""" manualFiles: [String!]! @@ -619,6 +625,9 @@ type Mutation { """Check if a give spec file will match the project spec pattern""" matchesSpecPattern(specFile: String!): Boolean! + """Transforms cypress.json file into cypress.config.js file""" + migrateConfigFile: Boolean + """Open a path in preferred IDE""" openDirectoryInIDE(path: String!): Boolean openExternal(url: String!): Boolean diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts index ba31f24e46..ba67ad1d08 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Migration.ts @@ -14,7 +14,7 @@ export const Migration = objectType({ t.nonNull.field('step', { type: MigrationStepEnum, description: 'Step where the migration is right now', - resolve: () => 'renameManual', + resolve: () => 'configFile', }) t.nonNull.list.nonNull.string('specFilesBefore', { @@ -40,15 +40,29 @@ export const Migration = objectType({ t.nonNull.string('configBeforeCode', { description: 'contents of the cypress.json file before conversion', - resolve: () => { - return `` + resolve: (source, args, ctx) => { + return ctx.migration.getConfig() }, }) t.nonNull.string('configAfterCode', { description: 'contents of the cypress.json file after conversion', - resolve: () => { - return `` + resolve: (source, args, ctx) => { + return ctx.migration.createConfigString() + }, + }) + + t.nonNull.string('integrationFolder', { + description: 'the integration folder path used to store e2e tests', + resolve: (source, args, ctx) => { + return ctx.migration.getIntegrationFolder() + }, + }) + + t.nonNull.string('componentFolder', { + description: 'the component folder path used to store components tests', + resolve: (source, args, ctx) => { + return ctx.migration.getComponentFolder() }, }) }, diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 61b7704709..b97d779b94 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -419,6 +419,20 @@ export const mutation = mutationType({ }, }) + t.field('migrateConfigFile', { + description: 'Transforms cypress.json file into cypress.config.js file', + type: 'Boolean', + resolve: async (_, args, ctx) => { + try { + await ctx.actions.migration.createConfigFile() + + return true + } catch { + return false + } + }, + }) + t.field('setProjectIdInConfigFile', { description: 'Set the projectId field in the config file of the current project', type: Query, diff --git a/packages/launchpad/cypress/e2e/migration.cy.ts b/packages/launchpad/cypress/e2e/migration.cy.ts new file mode 100644 index 0000000000..69a8dbe61a --- /dev/null +++ b/packages/launchpad/cypress/e2e/migration.cy.ts @@ -0,0 +1,41 @@ +describe('Migration', () => { + beforeEach(() => { + cy.scaffoldProject('migration') + cy.openProject('migration') + cy.visitLaunchpad() + }) + + describe('Configuration', () => { + it('should create the cypress.config.js file and delete old config', () => { + cy.get('[data-cy="convertConfigButton"]').click() + + cy.withCtx(async (ctx) => { + const stats = await ctx.actions.file.checkIfFileExists('cypress.config.js') + + expect(stats).to.not.be.null.and.not.be.undefined + + let doesFileExist = true + + try { + await ctx.actions.file.checkIfFileExists('cypress.json') + } catch (error) { + doesFileExist = false + } + + expect(doesFileExist).to.be.false + }) + }) + + it('should create a valid js file', () => { + cy.get('[data-cy="convertConfigButton"]').click() + + cy.withCtx(async (ctx) => { + const configPath = ctx.path.join(ctx.lifecycleManager.projectRoot, 'cypress.config.js') + + const isValidJsFile = ctx.file.isValidJsFile(configPath) + + expect(isValidJsFile).to.be.true + }) + }) + }) +}) diff --git a/packages/launchpad/src/Main.vue b/packages/launchpad/src/Main.vue index 4e2261def4..0d50676f6e 100644 --- a/packages/launchpad/src/Main.vue +++ b/packages/launchpad/src/Main.vue @@ -13,8 +13,7 @@ :gql="query.data.value" /> diff --git a/system-tests/projects/migration/cypress.json b/system-tests/projects/migration/cypress.json new file mode 100644 index 0000000000..279a1128a8 --- /dev/null +++ b/system-tests/projects/migration/cypress.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://on.cypress.io/cypress.schema.json", + "baseUrl": "http://localhost:3000", + "retries": 2, + "defaultCommandTimeout": 5000, + "fixturesFolder": false, + "componentFolder": "src", + "testFiles": "**/*.spec.{tsx,js}", + "pluginsFile": "cypress/plugins/index.ts", + "e2e": { + "defaultCommandTimeout": 10000, + "slowTestThreshold": 5000 + }, + "component": { + "slowTestThreshold": 5000, + "retries": 1 + } +} \ No newline at end of file diff --git a/system-tests/projects/migration/cypress/plugins/index.js b/system-tests/projects/migration/cypress/plugins/index.js new file mode 100644 index 0000000000..af7dd141fc --- /dev/null +++ b/system-tests/projects/migration/cypress/plugins/index.js @@ -0,0 +1,5 @@ +module.exports = (on, config) => { + return { + test: 'value', + } +}