diff --git a/npm/eslint-plugin-dev/package.json b/npm/eslint-plugin-dev/package.json index 35344ec2e1d4..f01cdbb88b9a 100644 --- a/npm/eslint-plugin-dev/package.json +++ b/npm/eslint-plugin-dev/package.json @@ -21,8 +21,8 @@ "eslint-plugin-json-format": "^2.0.0", "eslint-plugin-mocha": "^8.1.0", "eslint-plugin-promise": "^4.2.1", - "jest": "^24.8.0", - "jest-cli": "^24.8.0", + "jest": "^24.9.0", + "jest-cli": "^24.9.0", "sinon": "^7.3.2", "sinon-chai": "^3.3.0" }, diff --git a/package.json b/package.json index 50ac2fa07806..89de2a848a40 100644 --- a/package.json +++ b/package.json @@ -261,7 +261,10 @@ ], "nohoist": [ "**/webpack-preprocessor/babel-loader", - "**/webpack-batteries-included-preprocessor/ts-loader" + "**/webpack-batteries-included-preprocessor/ts-loader", + "**/jest", + "**/jest*", + "**/expect" ] }, "lint-staged": { diff --git a/packages/app/src/settings/project/ExperimentRow.vue b/packages/app/src/settings/project/ExperimentRow.vue index 690fc474bf43..b8b0ed67ea52 100644 --- a/packages/app/src/settings/project/ExperimentRow.vue +++ b/packages/app/src/settings/project/ExperimentRow.vue @@ -64,11 +64,11 @@ const { t } = useI18n() diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index 80bb13036219..bbc59dd55fcf 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -492,6 +492,7 @@ export class ProjectActions { async reconfigureProject () { await this.ctx.actions.browser.closeBrowser() this.ctx.actions.wizard.resetWizard() + await this.ctx.actions.wizard.initialize() this.ctx.actions.electron.refreshBrowserWindow() this.ctx.actions.electron.showBrowserWindow() } diff --git a/packages/data-context/src/actions/WizardActions.ts b/packages/data-context/src/actions/WizardActions.ts index b9b1706510fd..c1f87cd3598b 100644 --- a/packages/data-context/src/actions/WizardActions.ts +++ b/packages/data-context/src/actions/WizardActions.ts @@ -1,9 +1,11 @@ import type { CodeLanguageEnum, NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' -import { Bundler, CodeLanguage, CODE_LANGUAGES, FrontendFramework } from '@packages/types' +import { Bundler, CodeLanguage, CODE_LANGUAGES, FrontendFramework, FRONTEND_FRAMEWORKS } from '@packages/types' import assert from 'assert' import dedent from 'dedent' -import fs from 'fs' import path from 'path' +import Debug from 'debug' + +const debug = Debug('cypress:data-context:wizard-actions') import type { DataContext } from '..' @@ -27,13 +29,24 @@ export class WizardActions { } setFramework (framework: NexusGenEnums['FrontendFrameworkEnum'] | null) { + const prevFramework = this.ctx.coreData.wizard.chosenFramework || '' + this.ctx.coreData.wizard.chosenFramework = framework if (framework !== 'react' && framework !== 'vue') { return this.setBundler('webpack') } - return this.setBundler(null) + const { chosenBundler } = this.ctx.coreData.wizard + + // if the previous bundler was incompatible with the + // new framework, we need to reset it + if ((chosenBundler && !this.ctx.wizard.chosenFramework?.supportedBundlers.includes(chosenBundler)) + || !['react', 'vue'].includes(prevFramework)) { + return this.setBundler(null) + } + + return } setBundler (bundler: NexusGenEnums['SupportedBundlers'] | null) { @@ -48,7 +61,13 @@ export class WizardActions { return this.data } - completeSetup () { + async completeSetup () { + debug('completeSetup') + // wait for the config to be initialized if it is not yet + // before returning. This should not penalize users but + // allow for tests, too fast for this last step to pass. + // NOTE: if the config is already initialized, this will be instant + await this.ctx.lifecycleManager.initializeConfig() this.ctx.update((d) => { d.scaffoldedFiles = null }) @@ -63,6 +82,92 @@ export class WizardActions { return this.data } + async initialize () { + if (this.ctx.currentProject) { + this.data.detectedFramework = null + this.data.detectedBundler = null + this.data.detectedLanguage = null + + await this.detectLanguage() + debug('detectedLanguage %s', this.data.detectedLanguage) + this.data.chosenLanguage = this.data.detectedLanguage || 'js' + + let hasPackageJson = true + + try { + await this.ctx.fs.access(path.join(this.ctx.currentProject, 'package.json'), this.ctx.fs.constants.R_OK) + } catch (e) { + debug('Could not read or find package.json: %O', e) + hasPackageJson = false + } + const packageJson: { + dependencies?: { [key: string]: string } + devDependencies?: { [key: string]: string } + } = hasPackageJson ? await this.ctx.fs.readJson(path.join(this.ctx.currentProject, 'package.json')) : {} + + debug('packageJson %O', packageJson) + const dependencies = [ + ...Object.keys(packageJson.dependencies || {}), + ...Object.keys(packageJson.devDependencies || {}), + ] + + this.detectFramework(dependencies) + debug('detectedFramework %s', this.data.detectedFramework) + this.detectBundler(dependencies) + debug('detectedBundler %s', this.data.detectedBundler) + + this.data.chosenFramework = this.data.detectedFramework || null + this.data.chosenBundler = this.data.detectedBundler || null + } + } + + private detectFramework (dependencies: string[]) { + // Detect full featured frameworks + if (dependencies.includes('next')) { + this.ctx.wizardData.detectedFramework = 'nextjs' + } else if (dependencies.includes('react-scripts')) { + this.ctx.wizardData.detectedFramework = 'cra' + } else if (dependencies.includes('nuxt')) { + this.ctx.wizardData.detectedFramework = 'nuxtjs' + } else if (dependencies.includes('@vue/cli-service')) { + this.ctx.wizardData.detectedFramework = 'vuecli' + } else if (dependencies.includes('react')) { + this.ctx.wizardData.detectedFramework = 'react' + } else if (dependencies.includes('vue')) { + this.ctx.wizardData.detectedFramework = 'vue' + } + } + + private detectBundler (dependencies: string[]) { + const detectedFrameworkObject = FRONTEND_FRAMEWORKS.find((f) => f.type === this.ctx.wizardData.detectedFramework) + + if (detectedFrameworkObject && detectedFrameworkObject.supportedBundlers.length === 1) { + this.ctx.wizardData.detectedBundler = detectedFrameworkObject.supportedBundlers[0] ?? null + + return + } + + if (dependencies.includes('webpack')) { + this.ctx.wizardData.detectedBundler = 'webpack' + } + + if (dependencies.includes('vite')) { + this.ctx.wizardData.detectedBundler = 'vite' + } + } + + private async detectLanguage () { + const { hasTypescript } = this.ctx.lifecycleManager.metaState + + if ( + hasTypescript || + (this.ctx.lifecycleManager.configFile && /.ts$/.test(this.ctx.lifecycleManager.configFile))) { + this.ctx.wizardData.detectedLanguage = 'ts' + } else { + this.ctx.wizardData.detectedLanguage = 'js' + } + } + /** * Scaffolds the testing type, by creating the necessary files & assigning to */ @@ -103,6 +208,7 @@ export class WizardActions { } private async scaffoldComponent () { + debug('scaffoldComponent') const { chosenBundler, chosenFramework, chosenLanguage } = this.ctx.wizard assert(chosenFramework && chosenLanguage && chosenBundler) @@ -157,58 +263,43 @@ export class WizardActions { } private async scaffoldConfig (testingType: 'e2e' | 'component'): Promise { - if (!fs.existsSync(this.ctx.lifecycleManager.configFilePath)) { - this.ctx.lifecycleManager.setConfigFilePath(this.ctx.coreData.wizard.chosenLanguage) + debug('scaffoldConfig') - const configCode = this.configCode(testingType, this.ctx.coreData.wizard.chosenLanguage) + if (this.ctx.lifecycleManager.metaState.hasValidConfigFile) { + const { ext } = path.parse(this.ctx.lifecycleManager.configFilePath) + const foundLanguage = ext === '.ts' ? 'ts' : 'js' + const configCode = this.configCode(testingType, foundLanguage) - return this.scaffoldFile( - this.ctx.lifecycleManager.configFilePath, - configCode, - 'Created a new config file', - ) + return { + status: 'changes', + description: 'Merge this code with your existing config file', + file: { + absolute: this.ctx.lifecycleManager.configFilePath, + contents: configCode, + }, + } } - const { ext } = path.parse(this.ctx.lifecycleManager.configFilePath) + const configCode = this.configCode(testingType, this.ctx.coreData.wizard.chosenLanguage) - const configCode = this.configCode(testingType, ext === '.ts' ? 'ts' : 'js') + // only do this if config file doesn't exist + this.ctx.lifecycleManager.setConfigFilePath(this.ctx.coreData.wizard.chosenLanguage) - return { - status: 'changes', - description: 'Merge this code with your existing config file', - file: { - absolute: this.ctx.lifecycleManager.configFilePath, - contents: configCode, - }, - } + return this.scaffoldFile( + this.ctx.lifecycleManager.configFilePath, + configCode, + 'Created a new config file', + ) } private async scaffoldFixtures (): Promise { const exampleScaffoldPath = path.join(this.projectRoot, 'cypress/fixtures/example.json') - try { - await this.ctx.fs.stat(exampleScaffoldPath) + await this.ensureDir('fixtures') - return { - status: 'skipped', - file: { - absolute: exampleScaffoldPath, - contents: '// Skipped', - }, - description: 'Fixtures directory already exists, skipping', - } - } catch (e) { - await this.ensureDir('fixtures') - await this.ctx.fs.writeFile(exampleScaffoldPath, `${JSON.stringify(FIXTURE_DATA, null, 2)}\n`) - - return { - status: 'valid', - description: 'Added an example fixtures file/folder', - file: { - absolute: exampleScaffoldPath, - }, - } - } + return this.scaffoldFile(exampleScaffoldPath, + `${JSON.stringify(FIXTURE_DATA, null, 2)}\n`, + 'Added an example fixtures file/folder') } private wizardGetConfigCodeE2E (lang: CodeLanguageEnum): string { @@ -303,18 +394,11 @@ export class WizardActions { } private async scaffoldFile (filePath: string, contents: string, description: string): Promise { - if (fs.existsSync(filePath)) { - return { - status: 'skipped', - description: 'File already exists', - file: { - absolute: filePath, - }, - } - } - try { - await this.ctx.fs.writeFile(filePath, contents) + debug('scaffoldFile: start %s', filePath) + debug('scaffoldFile: with content', contents) + await this.ctx.fs.writeFile(filePath, contents, { flag: 'wx' }) + debug('scaffoldFile: done %s', filePath) return { status: 'valid', @@ -324,6 +408,16 @@ export class WizardActions { }, } } catch (e: any) { + if (e.code === 'EEXIST') { + return { + status: 'skipped', + description: 'File already exists', + file: { + absolute: filePath, + }, + } + } + return { status: 'error', description: e.message || 'Error writing file', diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index a838035d7063..97b62610db34 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -545,6 +545,7 @@ export class ProjectLifecycleManager { promise.then((result) => { if (this._configResult.value === promise) { + debug(`config is loaded for file`, this.configFilePath) this._configResult = { state: 'loaded', value: result } this.validateConfigFile(this.configFilePath, result.initialConfig) this.onConfigLoaded(child, ipc, result) @@ -614,24 +615,32 @@ export class ProjectLifecycleManager { return } - const legacyFileWatcher = this.addWatcher(_.without([ + const legacyFileWatcher = this.addWatcher([ this._pathToFile('cypress.json'), this._pathToFile('cypress.config.js'), this._pathToFile('cypress.config.ts'), - ], this.configFilePath)) + ]) + + legacyFileWatcher.on('all', (event, file) => { + debug('WATCHER: config file event', event, file) + let shouldReloadConfig = this.configFile === file - legacyFileWatcher.on('all', (change) => { - const metaState = this._projectMetaState - const nextMetaState = this.refreshMetaState() + if (!shouldReloadConfig) { + const metaState = this._projectMetaState + const nextMetaState = this.refreshMetaState() + + shouldReloadConfig = !_.isEqual(metaState, nextMetaState) + } - if (!_.isEqual(metaState, nextMetaState)) { + if (shouldReloadConfig) { this.ctx.coreData.baseError = null this.reloadConfig().catch(this.onLoadError) } + }).on('error', (err) => { + debug('error watching config files %O', err) + this.ctx.coreData.baseError = err }) - this.initializeConfigFileWatcher() - const cypressEnvFileWatcher = this.addWatcher(this.envFilePath) cypressEnvFileWatcher.on('all', () => { @@ -640,15 +649,6 @@ export class ProjectLifecycleManager { }) } - initializeConfigFileWatcher () { - this._configWatcher = this.addWatcher(this.configFilePath) - - this._configWatcher.on('all', () => { - this.ctx.coreData.baseError = null - this.reloadConfig().catch(this.onLoadError) - }) - } - /** * When we detect a change to the config file path, we call "reloadConfig". * This sources a fresh IPC channel & reads the config. If we detect a change @@ -657,11 +657,14 @@ export class ProjectLifecycleManager { reloadConfig () { if (this._configResult.state === 'errored' || this._configResult.state === 'loaded') { this._configResult = { state: 'pending' } + debug('reloadConfig refresh') return this.initializeConfig() } if (this._configResult.state === 'loading' || this._configResult.state === 'pending') { + debug('reloadConfig first load') + return this.initializeConfig() } @@ -1165,14 +1168,7 @@ export class ProjectLifecycleManager { } setConfigFilePath (lang: 'ts' | 'js') { - const configFilePath = this._configFilePath - this._configFilePath = this._pathToFile(`cypress.config.${lang}`) - - if (configFilePath !== this._configFilePath && this._configWatcher) { - this.closeWatcher(this._configWatcher) - this.initializeConfigFileWatcher() - } } private _pathToFile (file: string) { diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 1325a529723b..8aa6d019fc79 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -66,6 +66,9 @@ export interface WizardDataShape { chosenFramework: NexusGenEnums['FrontendFrameworkEnum'] | null chosenLanguage: NexusGenEnums['CodeLanguageEnum'] chosenManualInstall: boolean + detectedLanguage: NexusGenEnums['CodeLanguageEnum'] | null + detectedBundler: NexusGenEnums['SupportedBundlers'] | null + detectedFramework: NexusGenEnums['FrontendFrameworkEnum'] | null } export interface MigrationDataShape{ @@ -158,6 +161,9 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa chosenLanguage: 'js', chosenManualInstall: false, allBundlers: BUNDLERS, + detectedBundler: null, + detectedFramework: null, + detectedLanguage: null, }, migration: { step: 'renameAuto', diff --git a/packages/data-context/src/sources/MigrationDataSource.ts b/packages/data-context/src/sources/MigrationDataSource.ts index f5ff691f4a1c..997914531b6d 100644 --- a/packages/data-context/src/sources/MigrationDataSource.ts +++ b/packages/data-context/src/sources/MigrationDataSource.ts @@ -68,6 +68,7 @@ export class MigrationDataSource { private componentTestingMigrationWatcher?: chokidar.FSWatcher componentTestingMigrationStatus?: ComponentTestingMigrationStatus + private _oldConfigPromise: Promise | null = null constructor (private ctx: DataContext) { } @@ -80,6 +81,7 @@ export class MigrationDataSource { } this._config = null + this._oldConfigPromise = null const config = await this.parseCypressConfig() await this.initializeFlags() @@ -230,10 +232,17 @@ export class MigrationDataSource { return this._config } - if (this.ctx.lifecycleManager.metaState.hasLegacyCypressJson) { + // 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, 'cypress.json') - this._config = await this.ctx.file.readJsonFile(cfgPath) as OldCypressConfig + this._oldConfigPromise = this.ctx.file.readJsonFile(cfgPath) as Promise + } + + if (this._oldConfigPromise) { + this._config = await this._oldConfigPromise + + this._oldConfigPromise = null return this._config } diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Wizard.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Wizard.ts index 277ee94588c5..5a5a6153607a 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Wizard.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Wizard.ts @@ -7,6 +7,7 @@ export const allBundlers = BUNDLERS.map((bundler, idx) => { return { ...testNodeId('WizardBundler'), isSelected: idx === 0, + isDetected: false, ...bundler, } }) @@ -37,6 +38,7 @@ export const stubWizard: MaybeResolver = { ...framework, supportedBundlers, isSelected: idx === 0, + isDetected: false, } }), language: { @@ -44,12 +46,14 @@ export const stubWizard: MaybeResolver = { type: 'ts', name: 'TypeScript', isSelected: true, + isDetected: false, }, allLanguages: CODE_LANGUAGES.map((language, idx) => { return { ...testNodeId('WizardCodeLanguage'), ...language, isSelected: idx === 0, + isDetected: false, } }), } diff --git a/packages/frontend-shared/src/components/ShikiHighlight.vue b/packages/frontend-shared/src/components/ShikiHighlight.vue index c7edc83d9c0d..a320814f4e19 100644 --- a/packages/frontend-shared/src/components/ShikiHighlight.vue +++ b/packages/frontend-shared/src/components/ShikiHighlight.vue @@ -63,7 +63,7 @@ shikiWrapperClasses computed property. v-if="copyButton && isSupported" variant="outline" tabindex="-1" - class="absolute" + class="bg-white absolute" :class="numberOfLines === 1 ? 'bottom-5px right-5px' : 'bottom-8px right-8px'" :text="code" no-icon diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 4f9023f47d45..a48b5ef93df9 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -238,9 +238,11 @@ "frameworkLabel": "Front-end Framework", "frameworkPlaceholder": "Pick a framework", "bundlerLabel": "Bundler", + "bundlerLabelDescription": "(Dev Server)", "bundlerPlaceholder": "Pick a bundler", "languageLabel": "Language", - "configFileLanguageLabel": "Cypress Config File" + "configFileLanguageLabel": "Cypress Config File", + "detected": "(detected)" }, "install": { "startButton": "Install", diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index bd9c5bbdaaeb..c8a9b6473e43 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -1097,6 +1097,9 @@ type WizardBundler implements Node { """Relay style Node ID field for the WizardBundler field""" id: ID! + """Whether this is the detected bundler""" + isDetected: Boolean! + """Whether this is the selected framework bundler""" isSelected: Boolean @@ -1117,6 +1120,9 @@ type WizardCodeLanguage implements Node { """Relay style Node ID field for the WizardCodeLanguage field""" id: ID! + """Whether this is the detected language""" + isDetected: Boolean! + """Whether this is the selected language in the wizard""" isSelected: Boolean! @@ -1142,6 +1148,9 @@ type WizardFrontendFramework implements Node { """Relay style Node ID field for the WizardFrontendFramework field""" id: ID! + """Whether this is the detected framework""" + isDetected: Boolean! + """Whether this is the selected framework in the wizard""" isSelected: Boolean! diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts index 7984b39a0823..be79e3bad420 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-Mutation.ts @@ -99,7 +99,7 @@ export const mutation = mutationType({ t.field('completeSetup', { type: 'Query', resolve: async (_, args, ctx) => { - ctx.actions.wizard.completeSetup() + await ctx.actions.wizard.completeSetup() return {} }, @@ -130,9 +130,19 @@ export const mutation = mutationType({ args: { testingType: nonNull(arg({ type: TestingTypeEnum })), }, - resolve: (source, args, ctx) => { + resolve: async (source, args, ctx) => { ctx.actions.project.setCurrentTestingType(args.testingType) + // if necessary init the wizard for configuration + if (ctx.coreData.currentTestingType + && !ctx.lifecycleManager.isTestingTypeConfigured(ctx.coreData.currentTestingType)) { + try { + await ctx.actions.wizard.initialize() + } catch (e) { + ctx.coreData.baseError = e as Error + } + } + return {} }, }) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardBundler.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardBundler.ts index ff544ef1d7b4..8eba054c41e8 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardBundler.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardBundler.ts @@ -11,6 +11,11 @@ export const WizardBundler = objectType({ resolve: (source, args, ctx) => ctx.wizardData.chosenBundler === source.type, }) + t.nonNull.boolean('isDetected', { + description: 'Whether this is the detected bundler', + resolve: (source, args, ctx) => ctx.wizardData.detectedBundler === source.type, + }) + t.nonNull.field('type', { type: SupportedBundlerEnum, description: 'The name of the framework', diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardCodeLanguage.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardCodeLanguage.ts index f0f2571bf365..bdeb61a8c7de 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardCodeLanguage.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardCodeLanguage.ts @@ -16,6 +16,11 @@ export const WizardCodeLanguage = objectType({ resolve: (source, args, ctx) => ctx.wizardData.chosenLanguage === source.type, }) + t.nonNull.boolean('isDetected', { + description: 'Whether this is the detected language', + resolve: (source, args, ctx) => ctx.wizardData.detectedLanguage === source.type, + }) + t.nonNull.string('name', { description: 'The name of the language', }) diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts index e7d18908305f..ef14105a7957 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-WizardFrontendFramework.ts @@ -22,6 +22,11 @@ export const WizardFrontendFramework = objectType({ resolve: (source, args, ctx) => ctx.wizardData.chosenFramework === source.type, }) + t.nonNull.boolean('isDetected', { + description: 'Whether this is the detected framework', + resolve: (source, args, ctx) => ctx.wizardData.detectedFramework === source.type, + }) + t.nonNull.string('name', { description: 'The name of the framework', }) diff --git a/packages/launchpad/cypress/e2e/error-handling.cy.ts b/packages/launchpad/cypress/e2e/error-handling.cy.ts index a3ac61834802..84ae9ea4b75d 100644 --- a/packages/launchpad/cypress/e2e/error-handling.cy.ts +++ b/packages/launchpad/cypress/e2e/error-handling.cy.ts @@ -28,13 +28,18 @@ describe('Error handling', () => { it('it handles a configuration file error', () => { cy.scaffoldProject('pristine') - .then(() => { - cy.openProject('pristine') - cy.withCtx(async (ctx) => { - await ctx.actions.file.writeFileInProject('cypress.config.js', 'throw new Error("Error thrown from Config")') - }) + + // sets the current project to enable writeFileInProject + cy.openProject('pristine') + + // write a bad config file + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('cypress.config.js', 'throw new Error("Error thrown from Config")') }) + // reopens the project with the new config file (CircleCI does not react to the addition of a file) + cy.openProject('pristine') + cy.visitLaunchpad() cy.get('body') diff --git a/packages/launchpad/cypress/e2e/migration.cy.ts b/packages/launchpad/cypress/e2e/migration.cy.ts index 8070feb900f9..a879d3c97819 100644 --- a/packages/launchpad/cypress/e2e/migration.cy.ts +++ b/packages/launchpad/cypress/e2e/migration.cy.ts @@ -50,6 +50,10 @@ function finishMigrationAndContinue () { cy.contains('Finish migration and continue').click() } +function checkOutcome () { + cy.contains('Welcome to Cypress!').should('be.visible') +} + function runAutoRename () { cy.get('button').contains('Rename these specs for me').click() } @@ -87,6 +91,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo skipCTMigration() migrateAndVerifyConfig() finishMigrationAndContinue() + checkOutcome() }) it('completes journey for migration-component-testing-defaults', () => { @@ -125,6 +130,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo skipCTMigration() migrateAndVerifyConfig() finishMigrationAndContinue() + checkOutcome() }) it('completes journey for migration-e2e-component-default-everything', () => { @@ -165,6 +171,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig() finishMigrationAndContinue() + checkOutcome() }) it('completes journey for migration-e2e-component-default-test-files', () => { @@ -204,6 +211,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig() finishMigrationAndContinue() + checkOutcome() }) it('completes journey for migration-e2e-custom-integration', () => { @@ -237,6 +245,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig() + checkOutcome() }) it('completes journey for migration-e2e-custom-test-files', () => { @@ -281,6 +290,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig() + checkOutcome() }) it('completes journey for migration-e2e-defaults', () => { @@ -324,6 +334,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport('ts') migrateAndVerifyConfig() + checkOutcome() }) it('completes journey for migration-e2e-no-plugins-support-file', () => { @@ -357,6 +368,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo runAutoRename() migrateAndVerifyConfig() + checkOutcome() }) // TODO: Do we need to consider this case? @@ -373,6 +385,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig() + checkOutcome() }) it('completes journey for migration-e2e-fully-custom', () => { @@ -387,6 +400,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo cy.get(configFileStep).should('exist') migrateAndVerifyConfig() + checkOutcome() }) it('completes journey for migration-component-testing-customized', () => { @@ -405,6 +419,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo skipCTMigration() migrateAndVerifyConfig() finishMigrationAndContinue() + checkOutcome() }) it('completes journey for migration-typescript-project', () => { @@ -448,6 +463,7 @@ describe('Full migration flow for each project', { retries: { openMode: 2, runMo renameSupport() migrateAndVerifyConfig('ts') + checkOutcome() }) }) diff --git a/packages/launchpad/cypress/e2e/project-setup.cy.ts b/packages/launchpad/cypress/e2e/project-setup.cy.ts index 1ecf8a691f6d..4c0fcef90842 100644 --- a/packages/launchpad/cypress/e2e/project-setup.cy.ts +++ b/packages/launchpad/cypress/e2e/project-setup.cy.ts @@ -1,15 +1,10 @@ import { FRONTEND_FRAMEWORKS, BUNDLERS, CODE_LANGUAGES, PACKAGES_DESCRIPTIONS } from '@packages/types/src/constants' describe('Launchpad: Setup Project', () => { - beforeEach(() => { - cy.scaffoldProject('pristine') // not configured - cy.scaffoldProject('pristine-with-ct-testing') // component configured - cy.scaffoldProject('pristine-with-e2e-testing') // e2e configured - cy.scaffoldProject('pristine-with-e2e-testing-and-storybook') // e2e configured - cy.scaffoldProject('pristine-npm') - cy.scaffoldProject('pristine-yarn') - cy.scaffoldProject('pristine-pnpm') - }) + function scaffoldAndOpenProject (name: Parameters[0], args?: Parameters[1]) { + cy.scaffoldProject(name) + cy.openProject(name, args) + } const verifyWelcomePage = ({ e2eIsConfigured, ctIsConfigured }) => { cy.contains('Welcome to Cypress!').should('be.visible') @@ -18,7 +13,7 @@ describe('Launchpad: Setup Project', () => { } it('no initial setup displays welcome page', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() cy.contains('Welcome to Cypress!').should('be.visible') verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -26,7 +21,7 @@ describe('Launchpad: Setup Project', () => { describe('"learn about testing types" modal', () => { beforeEach(() => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) }) @@ -137,7 +132,7 @@ describe('Launchpad: Setup Project', () => { describe('E2E test setup', () => { describe('project has been configured for e2e', () => { it('skips the setup page when choosing e2e tests to run', () => { - cy.openProject('pristine-with-e2e-testing') + scaffoldAndOpenProject('pristine-with-e2e-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: true }) @@ -148,7 +143,7 @@ describe('Launchpad: Setup Project', () => { }) it('opens to the browser pages when opened via cli with --e2e flag', () => { - cy.openProject('pristine-with-e2e-testing', ['--e2e']) + scaffoldAndOpenProject('pristine-with-e2e-testing', ['--e2e']) cy.visitLaunchpad() cy.get('h1').should('contain', 'Choose a Browser') @@ -159,7 +154,7 @@ describe('Launchpad: Setup Project', () => { describe('project that has not been configured for e2e', () => { // FIXME: ProjectLifecycleManager is skipping straight to browser pages when it should show setup page. it.skip('shows the configuration setup page when selecting e2e tests', () => { - cy.openProject('pristine-with-ct-testing') + scaffoldAndOpenProject('pristine-with-ct-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true }) @@ -181,7 +176,7 @@ describe('Launchpad: Setup Project', () => { // FIXME: ProjectLifecycleManager is skipping straight to browser pages when it should show setup page. it.skip('moves to "Choose a Browser" page after clicking "Continue" button in first step in configuration page', () => { - cy.openProject('pristine-with-ct-testing') + scaffoldAndOpenProject('pristine-with-ct-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true }) @@ -202,7 +197,7 @@ describe('Launchpad: Setup Project', () => { }) it('shows the configuration setup page when opened via cli with --e2e flag', () => { - cy.openProject('pristine-with-ct-testing', ['--e2e']) + scaffoldAndOpenProject('pristine-with-ct-testing', ['--e2e']) cy.visitLaunchpad() cy.contains('h1', 'Configuration Files') @@ -221,7 +216,7 @@ describe('Launchpad: Setup Project', () => { describe('project not been configured for cypress', () => { it('can go back before selecting e2e scaffold lang', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -241,7 +236,7 @@ describe('Launchpad: Setup Project', () => { }) it('can setup e2e testing for a project selecting JS', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -273,7 +268,7 @@ describe('Launchpad: Setup Project', () => { }) it('can setup e2e testing for a project selecting TS', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -299,7 +294,7 @@ describe('Launchpad: Setup Project', () => { }) it('can setup e2e testing for a project selecting TS when CT is configured and config file is JS', () => { - cy.openProject('pristine-with-ct-testing') + scaffoldAndOpenProject('pristine-with-ct-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true }) @@ -328,7 +323,7 @@ describe('Launchpad: Setup Project', () => { }) it('can setup CT testing for a project selecting TS when E2E is configured and config file is JS', () => { - cy.openProject('pristine-with-e2e-testing') + scaffoldAndOpenProject('pristine-with-e2e-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false }) @@ -348,18 +343,18 @@ describe('Launchpad: Setup Project', () => { cy.findByRole('button', { name: 'Back' }).click() cy.get('[data-cy-testingtype="component"]').click() - cy.findByRole('button', { name: 'Front-end Framework Create React App' }).click() + cy.findByRole('button', { name: 'Front-end Framework Pick a framework' }).click() cy.findByRole('option', { name: 'React.js' }).click() cy.findByRole('button', { name: 'Next Step' }).should('have.disabled') - cy.findByRole('button', { name: 'Bundler Pick a bundler' }).click() + cy.findByRole('button', { name: 'Bundler(Dev Server) Pick a bundler' }).click() cy.findByRole('option', { name: 'Webpack' }).click() cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled') cy.findByRole('button', { name: 'Front-end Framework React.js' }).click() cy.findByRole('option', { name: 'Create React App' }).click() - cy.findByRole('button', { name: 'Bundler Webpack' }).should('not.exist') + cy.findByRole('button', { name: 'Bundler(Dev Server) Webpack' }).should('not.exist') cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled') cy.findByRole('button', { name: 'TypeScript' }).click() @@ -405,7 +400,7 @@ describe('Launchpad: Setup Project', () => { }) it('shows the configuration setup page when opened via cli with --e2e flag', () => { - cy.openProject('pristine-with-ct-testing', ['--e2e']) + scaffoldAndOpenProject('pristine-with-ct-testing', ['--e2e']) cy.visitLaunchpad() cy.contains('h1', 'Configuration Files') @@ -422,7 +417,7 @@ describe('Launchpad: Setup Project', () => { }) it('can reconfigure config after CT has been set up', () => { - cy.openProject('pristine-with-ct-testing') + scaffoldAndOpenProject('pristine-with-ct-testing') cy.withCtx((ctx) => { ctx.coreData.forceReconfigureProject = { component: true, @@ -439,7 +434,7 @@ describe('Launchpad: Setup Project', () => { }) it('can reconfigure config after e2e has been set up', () => { - cy.openProject('pristine-with-e2e-testing') + scaffoldAndOpenProject('pristine-with-e2e-testing') cy.withCtx((ctx) => { ctx.coreData.forceReconfigureProject = { e2e: true, @@ -525,7 +520,7 @@ describe('Launchpad: Setup Project', () => { describe('Component setup', () => { describe('project has been configured for component testing', () => { it('skips the setup steps when choosing component tests to run', () => { - cy.openProject('pristine-with-ct-testing') + scaffoldAndOpenProject('pristine-with-ct-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: true }) @@ -536,7 +531,7 @@ describe('Launchpad: Setup Project', () => { }) it('opens to the browser pages when opened via cli with --component flag', () => { - cy.openProject('pristine-with-ct-testing', ['--component']) + scaffoldAndOpenProject('pristine-with-ct-testing', ['--component']) cy.visitLaunchpad() cy.get('h1').should('contain', 'Choose a Browser') @@ -565,7 +560,7 @@ describe('Launchpad: Setup Project', () => { }) it('shows the first setup page for configuration when selecting component tests', () => { - cy.openProject('pristine-with-e2e-testing') + scaffoldAndOpenProject('pristine-with-e2e-testing') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false }) @@ -611,7 +606,7 @@ describe('Launchpad: Setup Project', () => { } it(testTitle, () => { - cy.openProject(hasStorybookDep ? 'pristine-with-e2e-testing-and-storybook' : 'pristine-with-e2e-testing') + scaffoldAndOpenProject(hasStorybookDep ? 'pristine-with-e2e-testing-and-storybook' : 'pristine-with-e2e-testing') cy.withCtx((ctx) => { ctx.actions.file.writeFileInProject('yarn.lock', '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.') }) @@ -641,7 +636,7 @@ describe('Launchpad: Setup Project', () => { if (framework.supportedBundlers.length > 1) { cy.findByRole('button', { - name: 'Bundler Pick a bundler', + name: 'Bundler(Dev Server) Pick a bundler', expanded: false, }) .should('have.attr', 'aria-haspopup', 'true') @@ -659,7 +654,7 @@ describe('Launchpad: Setup Project', () => { .should('have.attr', 'data-cy', `${Cypress._.lowerCase(bundler.name)}-logo`) .click() - cy.findByRole('button', { name: `Bundler ${bundler.name}` }) // ensure selected option updates + cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` }) // ensure selected option updates } cy.findByRole('button', { name: lang.name }).click() @@ -676,7 +671,7 @@ describe('Launchpad: Setup Project', () => { cy.findByRole('button', { name: `Front-end Framework ${framework.name}` }) if (framework.supportedBundlers.length > 1) { - cy.findByRole('button', { name: `Bundler ${bundler.name}` }) + cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` }) } cy.findByRole('button', { name: lang.name }) @@ -729,7 +724,7 @@ describe('Launchpad: Setup Project', () => { }) it('opens to the "choose framework" page when opened via cli with --component flag', () => { - cy.openProject('pristine-with-e2e-testing', ['--component']) + scaffoldAndOpenProject('pristine-with-e2e-testing', ['--component']) cy.visitLaunchpad() cy.get('h1').should('contain', 'Project Setup') @@ -739,7 +734,7 @@ describe('Launchpad: Setup Project', () => { describe('project not been configured for cypress', () => { it('can setup component testing', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -759,27 +754,29 @@ describe('Launchpad: Setup Project', () => { cy.findByRole('button', { name: 'Back' }).click() cy.get('[data-cy-testingtype="component"]').click() - cy.findByRole('button', { name: 'Front-end Framework Create React App' }).click() + cy.findByRole('button', { name: 'Front-end Framework Pick a framework' }).click() cy.findByRole('option', { name: 'React.js' }).click() cy.findByRole('button', { name: 'Next Step' }).should('have.disabled') - cy.findByRole('button', { name: 'Bundler Pick a bundler' }).click() + cy.findByRole('button', { name: 'Bundler(Dev Server) Pick a bundler' }).click() cy.findByRole('option', { name: 'Webpack' }).click() + + cy.findByRole('button', { name: 'TypeScript' }).click() cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled') cy.findByRole('button', { name: 'Front-end Framework React.js' }).click() cy.findByRole('option', { name: 'Create React App' }).click() - cy.findByRole('button', { name: 'Bundler Webpack' }).should('not.exist') + cy.findByRole('button', { name: 'Bundler(Dev Server) Webpack' }).should('not.exist') cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled') cy.findByRole('button', { name: 'Next Step' }).click() cy.findByRole('button', { name: 'Continue' }).click() cy.get('[data-cy=valid]').within(() => { - cy.contains('cypress.config.js') + cy.contains('cypress.config.ts') cy.contains('cypress/component/index.html') - cy.contains(`cypress/support/component.js`) + cy.contains(`cypress/support/component.ts`) cy.contains('cypress/fixtures/example.json') }) @@ -788,7 +785,7 @@ describe('Launchpad: Setup Project', () => { }) it('setup component testing with typescript files', () => { - cy.openProject('pristine') + scaffoldAndOpenProject('pristine') cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: false, ctIsConfigured: false }) @@ -812,16 +809,15 @@ describe('Launchpad: Setup Project', () => { cy.contains('cypress/fixtures/example.json') }) - // FIXME: remove if-check once this is fixed. https://cypress-io.atlassian.net/browse/UNIFY-980 - // cy.findByRole('button', { name: 'Continue' }).click() - // cy.contains(/(Initializing Config|Choose a Browser)/) + cy.findByRole('button', { name: 'Continue' }).click() + cy.contains(/(Initializing Config|Choose a Browser)/) }) }) }) describe('Command for package managers', () => { it('makes the right command for yarn', () => { - cy.openProject('pristine-yarn') + scaffoldAndOpenProject('pristine-yarn') cy.visitLaunchpad() @@ -833,7 +829,7 @@ describe('Launchpad: Setup Project', () => { }) it('makes the right command for pnpm', () => { - cy.openProject('pristine-pnpm') + scaffoldAndOpenProject('pristine-pnpm') cy.visitLaunchpad() @@ -845,7 +841,7 @@ describe('Launchpad: Setup Project', () => { }) it('makes the right command for npm', () => { - cy.openProject('pristine-npm') + scaffoldAndOpenProject('pristine-npm') cy.visitLaunchpad() @@ -856,4 +852,160 @@ describe('Launchpad: Setup Project', () => { cy.get('code').should('contain.text', 'npm install -D ') }) }) + + describe('detect framework, bundler and language', () => { + beforeEach(() => { + scaffoldAndOpenProject('pristine') + }) + + context('meta frameworks', () => { + it('detects CRA framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "react": "^1.0.0", + "react-dom": "^1.0.1", + "react-scripts": "1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('Create React App').should('be.visible') + }) + + it('detects Next framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "react": "^1.0.0", + "react-dom": "^1.0.1", + "next": "1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('Next.js').should('be.visible') + }) + + it('detects vue-cli framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "vue": "^1.0.0", + "@vue/cli-service": "^1.0.1" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('Vue CLI').should('be.visible') + }) + + it('detects nuxtjs framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "vue": "^1.0.0", + "nuxt": "^1.0.1" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('Nuxt.js').should('be.visible') + }) + }) + + context('pure frameworks', () => { + it('detects react framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "react": "^1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('React.js').should('be.visible') + }) + + it('detects vue framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "vue": "^1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').findByText('Vue.js').should('be.visible') + }) + }) + + describe('bundlers', () => { + it('detects webpack framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "react": "^1.0.0", + "webpack": "^1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-bundler"]').findByText('Webpack').should('be.visible') + }) + + it('detects vite framework', () => { + cy.withCtx(async (ctx) => { + await ctx.actions.file.writeFileInProject('package.json', ` + { + "dependencies": { + "react": "^1.0.0", + "vite": "^1.0.0" + } + } + `) + }) + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-bundler"]').findByText('Vite').should('be.visible') + }) + }) + }) }) diff --git a/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx b/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx index 1b39ba8189e4..600174a008a0 100644 --- a/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx +++ b/packages/launchpad/src/setup/EnvironmentSetup.cy.tsx @@ -2,8 +2,8 @@ import { EnvironmentSetupFragmentDoc } from '../generated/graphql-test' import EnvironmentSetup from './EnvironmentSetup.vue' import { FRONTEND_FRAMEWORKS, CODE_LANGUAGES } from '../../../types/src/constants' -describe('', () => { - it('default component', { viewportWidth: 800 }, () => { +describe('', { viewportWidth: 800 }, () => { + it('default component', () => { cy.mountFragment(EnvironmentSetupFragmentDoc, { render: (gqlVal) => (
@@ -48,4 +48,48 @@ describe('', () => { cy.findByRole('button', { name: 'Next Step' }) .should('have.disabled') }) + + it('renders the detected flag', () => { + cy.mountFragment(EnvironmentSetupFragmentDoc, { + onResult: (res) => { + res.frameworks[0].isDetected = true + }, + render: (gqlVal) => ( +
+ +
+ ), + }) + + cy.findByRole('button', { + name: 'Front-end Framework Pick a framework', + expanded: false, + }).click() + + cy.findByRole('option', { name: 'Create React App (detected)' }).should('be.visible') + }) + + it('shows the description of bundler as Dev Server', () => { + cy.mountFragment(EnvironmentSetupFragmentDoc, { + onResult: (res) => { + res.framework = { + ...res.frameworks[3], + supportedBundlers: res.allBundlers, + } + }, + render: (gqlVal) => ( +
+ +
+ ), + }) + + cy.findByLabelText('Bundler(Dev Server)').should('be.visible') + }) }) diff --git a/packages/launchpad/src/setup/EnvironmentSetup.vue b/packages/launchpad/src/setup/EnvironmentSetup.vue index 457228e3880e..24c4898e2ded 100644 --- a/packages/launchpad/src/setup/EnvironmentSetup.vue +++ b/packages/launchpad/src/setup/EnvironmentSetup.vue @@ -22,6 +22,7 @@ :value="props.gql.bundler?.type ?? undefined" :placeholder="t('setupPage.projectSetup.bundlerPlaceholder')" :label="t('setupPage.projectSetup.bundlerLabel')" + :description="t('setupPage.projectSetup.bundlerLabelDescription')" selector-type="bundler" data-testid="select-bundler" @select-bundler="val => onWizardSetup('bundler', val)" @@ -59,16 +60,19 @@ fragment EnvironmentSetup on Wizard { name type isSelected + isDetected } framework { type id name isSelected + isDetected supportedBundlers { id type name + isDetected } category } @@ -76,6 +80,7 @@ fragment EnvironmentSetup on Wizard { id name isSelected + isDetected type category } @@ -83,6 +88,7 @@ fragment EnvironmentSetup on Wizard { id name type + isDetected } language { id diff --git a/packages/launchpad/src/setup/ScaffoldLanguageSelect.vue b/packages/launchpad/src/setup/ScaffoldLanguageSelect.vue index 608354f02ac2..ae39f82a72a1 100644 --- a/packages/launchpad/src/setup/ScaffoldLanguageSelect.vue +++ b/packages/launchpad/src/setup/ScaffoldLanguageSelect.vue @@ -15,7 +15,7 @@
diff --git a/packages/launchpad/src/setup/ScaffoldedFiles.vue b/packages/launchpad/src/setup/ScaffoldedFiles.vue index 559bb8f5e8c3..e9d74d3da112 100644 --- a/packages/launchpad/src/setup/ScaffoldedFiles.vue +++ b/packages/launchpad/src/setup/ScaffoldedFiles.vue @@ -79,7 +79,7 @@ const needsChanges = computed(() => props.gql.scaffoldedFiles?.some((f) => f.sta const mutation = useMutation(ScaffoldedFiles_CompleteSetupDocument) -const completeSetup = () => { - mutation.executeMutation({}) +const completeSetup = async () => { + await mutation.executeMutation({}) } diff --git a/packages/launchpad/src/setup/SelectFwOrBundler.cy.tsx b/packages/launchpad/src/setup/SelectFwOrBundler.cy.tsx index 4f1ad74eaa5f..0c224b2557ac 100644 --- a/packages/launchpad/src/setup/SelectFwOrBundler.cy.tsx +++ b/packages/launchpad/src/setup/SelectFwOrBundler.cy.tsx @@ -8,6 +8,7 @@ const manyOptions = [ isSelected: false, type: 'vue', category: 'vue', + isDetected: true, }, { name: 'React.js', @@ -41,6 +42,18 @@ describe('', () => { cy.contains('Front-end Framework').should('exist') }) + it('shows detected flag', () => { + cy.mount(() => ()) + + cy.contains('React.js').click() + cy.contains('li', 'Vue.js').contains('(detected)').should('be.visible') + }) + it('shows a placeholder when no value is specified', () => { cy.mount(() => ( + + + + +