diff --git a/.eslintignore b/.eslintignore index ef89afbf8c..6247d1b9cb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -30,6 +30,7 @@ system-tests/projects/e2e/cypress/e2e/stdout_exit_early_failing.cy.js system-tests/projects/e2e/cypress/e2e/typescript_syntax_error.cy.ts system-tests/projects/config-with-ts-syntax-error/** system-tests/projects/config-with-ts-module-error/** +system-tests/projects/no-specs-vue-2/** **/test/fixtures diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 455f8a44ea..7e2d06eba4 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -195,6 +195,26 @@ describe('App: Specs', () => { cy.visitApp().get('[data-cy="spec-list-file"]').contains('MyTest.cy.js') }) + + it('should not show trouble rendering alert', () => { + cy.get('@EmptySpecCard').click() + + cy.findAllByLabelText(defaultMessages.createSpec.e2e.importEmptySpec.inputPlaceholder) + .as('enterSpecInput') + + // Create spec + cy.contains('button', defaultMessages.createSpec.createSpec).should('not.be.disabled').click() + cy.contains('h2', defaultMessages.createSpec.successPage.header) + + cy.get('[data-cy="file-row"]').contains(getPathForPlatform('cypress/e2e/spec.cy.ts')).click() + + cy.get('pre').should('contain', 'describe(\'empty spec\'') + + cy.findByRole('link', { name: 'Okay, run the spec' }) + .should('have.attr', 'href', `#/specs/runner?file=cypress/e2e/spec.cy.ts`).click() + + cy.contains('Review the docs').should('not.exist') + }) }) }) @@ -619,6 +639,98 @@ describe('App: Specs', () => { }) }) + context('Create from component card', () => { + beforeEach(() => { + cy.scaffoldProject('no-specs-vue-2') + cy.openProject('no-specs-vue-2') + cy.startAppServer('component') + cy.visitApp() + + cy.findAllByTestId('card').eq(0).as('ComponentCard') + }) + + it('Shows create from component card for Vue projects with default spec patterns', () => { + cy.get('@ComponentCard') + .within(() => { + cy.findByRole('button', { + name: 'Create from component', + }).should('be.visible') + .and('not.be.disabled') + }) + }) + + it('Can be closed with the x button', () => { + cy.get('@ComponentCard').click() + + cy.findByRole('button', { name: 'Close' }).as('DialogCloseButton') + + cy.get('@DialogCloseButton').click() + cy.findByRole('dialog', { + name: 'Choose a component', + }).should('not.exist') + }) + + it('Lists Vue components in the project', () => { + cy.get('@ComponentCard').click() + + cy.findByText('2 Matches').should('be.visible') + + cy.findByText('App').should('be.visible') + cy.findByText('HelloWorld').should('be.visible') + }) + + it('Allows for the user to search through their components', () => { + cy.get('@ComponentCard').click() + + cy.findByText('*.vue').should('be.visible') + cy.findByText('2 Matches').should('be.visible') + cy.findByLabelText('file-name-input').type('HelloWorld') + + cy.findByText('HelloWorld').should('be.visible') + cy.findByText('1 of 2 Matches').should('be.visible') + cy.findByText('App').should('not.exist') + }) + + it('shows success modal when component spec is created', () => { + cy.get('@ComponentCard').click() + + cy.findByText('HelloWorld').should('be.visible').click() + + cy.findByRole('dialog', { + name: defaultMessages.createSpec.successPage.header, + }).as('SuccessDialog').within(() => { + cy.contains(getPathForPlatform('src/components/HelloWorld.cy.js')).should('be.visible') + cy.findByRole('button', { name: 'Close' }).should('be.visible') + + cy.findByRole('link', { name: 'Okay, run the spec' }) + .should('have.attr', 'href', `#/specs/runner?file=src/components/HelloWorld.cy.js`) + + cy.findByRole('button', { name: 'Create another spec' }).click() + }) + + // 'Create from component' card appears again when the user selects "create another spec" + cy.findByText('Create from component').should('be.visible') + }) + + it('runs generated spec', () => { + cy.get('@ComponentCard').click() + + cy.findByText('HelloWorld').should('be.visible').click() + + cy.findByRole('dialog', { + name: defaultMessages.createSpec.successPage.header, + }).as('SuccessDialog').within(() => { + cy.contains(getPathForPlatform('src/components/HelloWorld.cy.js')).should('be.visible') + cy.findByRole('button', { name: 'Close' }).should('be.visible') + + cy.findByRole('link', { name: 'Okay, run the spec' }) + .should('have.attr', 'href', `#/specs/runner?file=src/components/HelloWorld.cy.js`).click() + }) + + cy.findByText('').should('be.visible') + }) + }) + context('project with custom spec pattern', () => { beforeEach(() => { cy.scaffoldProject('no-specs-custom-pattern') diff --git a/packages/app/src/specs/CreateSpecCard.vue b/packages/app/src/specs/CreateSpecCard.vue index 4cad03913b..0e3f79e618 100644 --- a/packages/app/src/specs/CreateSpecCard.vue +++ b/packages/app/src/specs/CreateSpecCard.vue @@ -5,9 +5,10 @@ :description="description" :icon="icon" :icon-size="48" - class="w-280px m-2px min-h-216px max-h-350px px-32px pt-32px - pb-24px" + class="m-2px min-h-216px max-h-350px px-32px pt-32px pb-24px + w-280px" variant="indigo" + :badge-text="badgeText" @click="emits('click')" /> @@ -21,6 +22,7 @@ defineProps<{ header: string description: string disabled?: boolean + badgeText?: string }>() const emits = defineEmits<{ diff --git a/packages/app/src/specs/CreateSpecCards.vue b/packages/app/src/specs/CreateSpecCards.vue index d479d95763..ace8d2530c 100644 --- a/packages/app/src/specs/CreateSpecCards.vue +++ b/packages/app/src/specs/CreateSpecCards.vue @@ -4,7 +4,6 @@ :is="generator.card" v-for="generator in generators" :key="generator.id" - :disabled="generator.disabled(props.gql.currentProject) || false" @click="$emit('select', generator.id)" /> @@ -15,7 +14,7 @@ import type { GeneratorId, SpecGenerator } from './generators' import type { CreateSpecCardsFragment } from '../generated/graphql' import { gql } from '@urql/vue' -const props = defineProps<{ +defineProps<{ generators: SpecGenerator[] gql: CreateSpecCardsFragment }>() @@ -28,6 +27,11 @@ gql` fragment CreateSpecCards on Query { currentProject { id + config + codeGenGlobs { + id + component + } } } ` diff --git a/packages/app/src/specs/CreateSpecModal.cy.tsx b/packages/app/src/specs/CreateSpecModal.cy.tsx index 2b79f7108c..fe55679d0a 100644 --- a/packages/app/src/specs/CreateSpecModal.cy.tsx +++ b/packages/app/src/specs/CreateSpecModal.cy.tsx @@ -15,6 +15,11 @@ describe('', () => { gql={{ currentProject: { id: 'id', + codeGenGlobs: { + id: 'super-unique-id', + __typename: 'CodeGenGlobs', + component: '**.vue', + }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', @@ -29,6 +34,7 @@ describe('', () => { specPattern: '**/*.cy.{js,jsx,ts,tsx}', }, }], + isDefaultSpecPattern: true, specs: [], fileExtensionToUse: 'js', defaultSpecFileName: 'cypress/e2e/ComponentName.cy.js', @@ -73,6 +79,11 @@ describe('Modal Text Input', () => { gql={{ currentProject: { id: 'id', + codeGenGlobs: { + id: 'super-unique-id', + __typename: 'CodeGenGlobs', + component: '**.vue', + }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', @@ -87,6 +98,7 @@ describe('Modal Text Input', () => { specPattern: '**/*.cy.{js,jsx,ts,tsx}', }, }], + isDefaultSpecPattern: true, specs: [], fileExtensionToUse: 'js', defaultSpecFileName: 'cypress/e2e/ComponentName.cy.js', @@ -116,6 +128,11 @@ describe('Modal Text Input', () => { gql={{ currentProject: { id: 'id', + codeGenGlobs: { + id: 'super-unique-id', + __typename: 'CodeGenGlobs', + component: '**.vue', + }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', @@ -130,6 +147,7 @@ describe('Modal Text Input', () => { specPattern: '**/*.cy.{js,jsx,ts,tsx}', }, }], + isDefaultSpecPattern: true, specs: [], fileExtensionToUse: 'js', defaultSpecFileName: 'this/path/does/not/produce/regex/match-', @@ -163,6 +181,11 @@ describe('playground', () => { gql={{ currentProject: { id: 'id', + codeGenGlobs: { + id: 'super-unique-id', + __typename: 'CodeGenGlobs', + component: '**.vue', + }, currentTestingType: 'component', configFile: 'cypress.config.js', configFileAbsolutePath: '/path/to/cypress.config.js', @@ -177,6 +200,7 @@ describe('playground', () => { specPattern: '**/*.cy.{js,jsx,ts,tsx}', }, }], + isDefaultSpecPattern: true, specs: [], fileExtensionToUse: 'js', defaultSpecFileName: 'cypress/e2e/ComponentName.cy.js', diff --git a/packages/app/src/specs/CreateSpecModal.vue b/packages/app/src/specs/CreateSpecModal.vue index ad5fbc7bef..13e8479934 100644 --- a/packages/app/src/specs/CreateSpecModal.vue +++ b/packages/app/src/specs/CreateSpecModal.vue @@ -21,7 +21,7 @@ :key="`${generator.id}-${iteration}`" v-model:title="title" :gql="props.gql.currentProject" - :type="props.gql.currentProject?.currentTestingType" + :type="props.gql.currentProject?.currentTestingType === 'e2e' ? props.gql.currentProject?.currentTestingType : 'componentEmpty'" :spec-file-name="specFileName" :other-generators="filteredGenerators.length > 1" @restart="currentGeneratorId = undefined; iteration++" @@ -77,6 +77,8 @@ fragment CreateSpecModal on Query { id fileExtensionToUse defaultSpecFileName + isDefaultSpecPattern + ...ComponentGeneratorStepOne_codeGenGlob ...EmptyGenerator } } @@ -105,7 +107,7 @@ const specFileName = computed(() => { return getPathForPlatform(props.gql.currentProject?.defaultSpecFileName || '') }) -const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject?.currentTestingType) +const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject, props.gql.currentProject?.isDefaultSpecPattern) const singleGenerator = computed(() => filteredGenerators.value.length === 1 ? filteredGenerators.value[0] : null) diff --git a/packages/app/src/specs/DefaultSpecPatternNoContent.vue b/packages/app/src/specs/DefaultSpecPatternNoContent.vue index 79a622bf84..3f0569ea04 100644 --- a/packages/app/src/specs/DefaultSpecPatternNoContent.vue +++ b/packages/app/src/specs/DefaultSpecPatternNoContent.vue @@ -50,6 +50,10 @@ fragment CreateSpecContent on Query { ...CreateSpecCards currentProject { id + codeGenGlobs { + id + component + } ...SpecPatternModal } } @@ -59,7 +63,7 @@ const props = defineProps<{ gql: CreateSpecContentFragment }>() -const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject?.currentTestingType) +const filteredGenerators = getFilteredGeneratorList(props.gql.currentProject, true) const emit = defineEmits<{ (e: 'showCreateSpecModal', id: string): void diff --git a/packages/app/src/specs/NoSpecsPage.cy.tsx b/packages/app/src/specs/NoSpecsPage.cy.tsx index 6b69e74863..352f6f4375 100644 --- a/packages/app/src/specs/NoSpecsPage.cy.tsx +++ b/packages/app/src/specs/NoSpecsPage.cy.tsx @@ -16,7 +16,7 @@ describe('', { viewportHeight: 655, viewportWidth: 1032 }, () => onResult: (ctx) => { ctx.currentProject = { ...ctx.currentProject!, - config: {}, + config: [{ field: 'specPattern', from: 'config', value: '**/*.cy.{js,jsx,ts,tsx}' }], id: 'id', configFileAbsolutePath: '/usr/bin/cypress.config.ts', currentTestingType: 'component', @@ -51,7 +51,7 @@ describe('', { viewportHeight: 655, viewportWidth: 1032 }, () => onResult: (ctx) => { ctx.currentProject = { ...ctx.currentProject!, - config: {}, + config: [{ field: 'specPattern', from: 'config', value: '**/*.cy.{js,jsx,ts,tsx}' }], configFileAbsolutePath: '/usr/bin/cypress.config.ts', id: 'id', currentTestingType: 'e2e', diff --git a/packages/app/src/specs/NoSpecsPage.vue b/packages/app/src/specs/NoSpecsPage.vue index db6555a677..0203f919c2 100644 --- a/packages/app/src/specs/NoSpecsPage.vue +++ b/packages/app/src/specs/NoSpecsPage.vue @@ -6,13 +6,13 @@

{{ props.title }}

{ + if (!isDefaultSpecPattern) { + return false + } + + return currentProject?.codeGenGlobs?.component === '*.vue' + }, + matches: filters.matchesCT, + id: 'component', +} diff --git a/packages/app/src/specs/generators/component/ComponentGeneratorCard.vue b/packages/app/src/specs/generators/component/ComponentGeneratorCard.vue new file mode 100644 index 0000000000..e245d7af6c --- /dev/null +++ b/packages/app/src/specs/generators/component/ComponentGeneratorCard.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue b/packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue new file mode 100644 index 0000000000..5f67b556d3 --- /dev/null +++ b/packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue @@ -0,0 +1,197 @@ + + + diff --git a/packages/app/src/specs/generators/component/index.ts b/packages/app/src/specs/generators/component/index.ts new file mode 100644 index 0000000000..996dd141ff --- /dev/null +++ b/packages/app/src/specs/generators/component/index.ts @@ -0,0 +1 @@ +export * from './ComponentGenerator' diff --git a/packages/app/src/specs/generators/empty/EmptyGenerator.tsx b/packages/app/src/specs/generators/empty/EmptyGenerator.tsx index 057756cde6..feccdc4200 100644 --- a/packages/app/src/specs/generators/empty/EmptyGenerator.tsx +++ b/packages/app/src/specs/generators/empty/EmptyGenerator.tsx @@ -6,6 +6,6 @@ export const EmptyGenerator: SpecGenerator = { card: EmptyGeneratorCard, entry: EmptyGeneratorCardStepOne, matches: () => true, - disabled: () => false, + show: () => true, id: 'empty', } diff --git a/packages/app/src/specs/generators/empty/EmptyGeneratorCard.vue b/packages/app/src/specs/generators/empty/EmptyGeneratorCard.vue index 2e753f78c0..fad7e3e15a 100644 --- a/packages/app/src/specs/generators/empty/EmptyGeneratorCard.vue +++ b/packages/app/src/specs/generators/empty/EmptyGeneratorCard.vue @@ -15,7 +15,7 @@ import DocumentCodeIcon from '~icons/cy/document-code_x48' const { t } = useI18n() defineProps<{ - disabled: boolean + disabled?: boolean }>() diff --git a/packages/app/src/specs/generators/index.ts b/packages/app/src/specs/generators/index.ts index aeeba51791..26bdd0013c 100644 --- a/packages/app/src/specs/generators/index.ts +++ b/packages/app/src/specs/generators/index.ts @@ -1,9 +1,9 @@ import { keyBy } from 'lodash' import { computed } from 'vue' -import type { TestingType } from '@packages/types' import type { SpecGenerator, GeneratorId } from './types' import { ScaffoldGenerator } from './scaffold' import { EmptyGenerator } from './empty' +import { ComponentGenerator } from './component' export * from './types' @@ -14,10 +14,13 @@ export * from './scaffold' export * from './empty' export const generatorList: SpecGenerator[] = [ + ComponentGenerator, ScaffoldGenerator, EmptyGenerator, ] -export const getFilteredGeneratorList = (testingType?: TestingType | null) => computed(() => generatorList.filter((g) => g.matches(testingType))) +export const getFilteredGeneratorList = (currentProject, isDefaultSpecPattern) => { + return computed(() => generatorList.filter((g) => g.matches(currentProject.currentTestingType) && (g.show === undefined ? true : g.show(currentProject, isDefaultSpecPattern)))) +} export const generators = keyBy(generatorList, 'id') as Record diff --git a/packages/app/src/specs/generators/scaffold/ScaffoldGenerator.tsx b/packages/app/src/specs/generators/scaffold/ScaffoldGenerator.tsx index d1dec6f8f7..0d3d3b9410 100644 --- a/packages/app/src/specs/generators/scaffold/ScaffoldGenerator.tsx +++ b/packages/app/src/specs/generators/scaffold/ScaffoldGenerator.tsx @@ -7,6 +7,6 @@ export const ScaffoldGenerator: SpecGenerator = { card: ScaffoldGeneratorCard, entry: ScaffoldGeneratorStepOne, matches: filters.matchesE2E, - disabled: () => { }, + show: () => true, id: 'scaffold', } diff --git a/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorCard.vue b/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorCard.vue index 75fc3481b0..c490ee1878 100644 --- a/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorCard.vue +++ b/packages/app/src/specs/generators/scaffold/ScaffoldGeneratorCard.vue @@ -15,7 +15,7 @@ import BoxOpenIcon from '~icons/cy/box-open_x48' const { t } = useI18n() defineProps<{ - disabled: boolean + disabled?: boolean }>() diff --git a/packages/app/src/specs/generators/types.ts b/packages/app/src/specs/generators/types.ts index dbce3f5b71..ebb70a715e 100644 --- a/packages/app/src/specs/generators/types.ts +++ b/packages/app/src/specs/generators/types.ts @@ -1,17 +1,20 @@ import type { TestingType } from '@packages/types' import type { Component } from 'vue' -export type GeneratorId = 'component' | 'empty' | 'scaffold' | 'story' +export type GeneratorId = 'component' | 'empty' | 'scaffold' type CurrentProject = { readonly __typename?: 'CurrentProject' | undefined readonly id: string + readonly codeGenGlobs?: { + readonly component: string + } } export interface SpecGenerator { card: Component entry: Component matches: (testingType?: TestingType | null) => boolean - disabled: (currentProject?: CurrentProject | null) => boolean | void + show: (currentProject?: CurrentProject, isDefaultSpecPattern?: boolean) => boolean id: GeneratorId } diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index e284545056..9f27a8606f 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -13,6 +13,7 @@ import templates from '../codegen/templates' import { insertValuesInConfigFile } from '../util' import { getError } from '@packages/errors' import { resetIssuedWarnings } from '@packages/config' +import { WizardFrontendFramework, WIZARD_FRAMEWORKS } from '@packages/scaffold-config' export interface ProjectApiShape { /** @@ -352,16 +353,18 @@ export class ProjectActions { const codeGenPath = getCodeGenPath() - const newSpecCodeGenOptions = new SpecOptions(this.ctx, { + const newSpecCodeGenOptions = new SpecOptions({ codeGenPath, codeGenType, erroredCodegenCandidate, + framework: this.getWizardFrameworkFromConfig(), + isDefaultSpecPattern: await this.ctx.project.getIsDefaultSpecPattern(), }) let codeGenOptions = await newSpecCodeGenOptions.getCodeGenOptions() const codeGenResults = await codeGenerator( - { templateDir: templates[codeGenType], target: path.parse(codeGenPath).dir }, + { templateDir: templates[codeGenOptions.templateKey], target: path.parse(codeGenPath).dir }, codeGenOptions, ) @@ -515,4 +518,16 @@ export class ProjectActions { await this.ctx.actions.wizard.scaffoldTestingType() } } + + getWizardFrameworkFromConfig (): WizardFrontendFramework | undefined { + const config = this.ctx.lifecycleManager.loadedConfigFile + + // If devServer is a function, they are using a custom dev server. + if (typeof config?.component?.devServer === 'function') { + return undefined + } + + // @ts-ignore - because of the conditional above, we know that devServer isn't a function + return WIZARD_FRAMEWORKS.find((framework) => framework.configFramework === config?.component?.devServer.framework) + } } diff --git a/packages/data-context/src/codegen/spec-options.ts b/packages/data-context/src/codegen/spec-options.ts index 951e825653..c5c6226140 100644 --- a/packages/data-context/src/codegen/spec-options.ts +++ b/packages/data-context/src/codegen/spec-options.ts @@ -1,12 +1,16 @@ -import assert from 'assert' -import type { DataContext } from '../DataContext' import type { ParsedPath } from 'path' import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen' +import type { WizardFrontendFramework } from '@packages/scaffold-config' +import fs from 'fs-extra' +import path from 'path' interface CodeGenOptions { codeGenPath: string codeGenType: CodeGenType + isDefaultSpecPattern: boolean erroredCodegenCandidate?: string | null + specFileExtension?: string + framework?: WizardFrontendFramework } // Spec file extensions that we will preserve when updating the file name @@ -18,18 +22,92 @@ interface CodeGenOptions { // Button.foo.js -> Button.foo-copy-1.js export const expectedSpecExtensions = ['.cy', '.spec', '.test', '-spec', '-test', '_spec'] +type ComponentExtension = `.cy.${'js' | 'ts' | 'jsx' | 'tsx'}` +type TemplateKey = 'e2e' | 'componentEmpty' | 'vueComponent' export class SpecOptions { private parsedPath: ParsedPath; + private parsedErroredCodegenCandidate?: ParsedPath - constructor (private ctx: DataContext, private options: CodeGenOptions) { - assert(this.ctx.currentProject) - this.parsedPath = this.ctx.path.parse(options.codeGenPath) + constructor (private options: CodeGenOptions) { + this.parsedPath = path.parse(options.codeGenPath) + + if (options.erroredCodegenCandidate) { + this.parsedErroredCodegenCandidate = path.parse(options.erroredCodegenCandidate) + } } async getCodeGenOptions () { + if (this.options.codeGenType === 'component') { + return this.getComponentCodeGenOptions() + } + return { codeGenType: this.options.codeGenType, - fileName: await this.getFilename(), + fileName: await this.buildFileName(), + templateKey: this.options.codeGenType as TemplateKey, + } + } + + private async getComponentCodeGenOptions () { + if (!this.options.framework) { + throw new Error('Cannot generate a spec without a framework') + } + + // This only works for Vue projects with default spec patterns right now. If the framework is not Vue, we're generating an empty component test + if (this.options.framework.codeGenFramework !== 'vue' || !this.options.isDefaultSpecPattern) { + return { + codeGenType: this.options.codeGenType, + fileName: await this.buildFileName(), + templateKey: 'componentEmpty' as TemplateKey, + } + } + + const frameworkOptions = await this.getFrameworkComponentOptions() + + return frameworkOptions + } + + private relativePath () { + if (!this.parsedErroredCodegenCandidate?.base) { + return `./${this.parsedPath.base}` + } + + const componentPathRelative = path.relative(this.parsedPath.dir, this.parsedErroredCodegenCandidate.dir) + + const componentPath = path.join(componentPathRelative, this.parsedErroredCodegenCandidate.base) + + return componentPath.startsWith('.') ? componentPath : `./${componentPath}` + } + + private async getFrameworkComponentOptions () { + const componentName = this.parsedErroredCodegenCandidate?.name ?? this.parsedPath.name + + const componentPath = this.relativePath() + + return { + codeGenType: this.options.codeGenType, + componentName, + componentPath, + fileName: await this.buildComponentSpecFilename(await this.getVueExtension()), + templateKey: 'vueComponent' as TemplateKey, + } + } + + private async getVueExtension (): Promise { + try { + const fileContent = await fs + .readFile(this.options.codeGenPath, 'utf8') + + return ['lang="ts"', 'lang="typescript"'].some((lang) => fileContent.includes(lang)) ? '.cy.ts' : '.cy.js' + } catch (e) { + const validExtensions = ['cy.js', '.cy.jsx', '.cy.ts', '.cy.tsx'] + const possibleExtension = this.parsedPath.ext + + if (validExtensions.includes(possibleExtension)) { + return possibleExtension as ComponentExtension + } + + return '.cy.js' } } @@ -45,31 +123,44 @@ export class SpecOptions { return foundSpecExtension || '' } - private async getFilename () { + private async buildComponentSpecFilename (specExt: string) { const { dir, base, ext } = this.parsedPath + const cyWithExt = this.getSpecExtension() + specExt + const name = base.slice(0, -ext.length) + + return this.getFinalFileName(dir, name, cyWithExt, path.join(dir, `${name}${cyWithExt}`)) + } + + private async buildFileName () { + const { dir, base, ext } = this.parsedPath + const cyWithExt = this.getSpecExtension() + ext const name = base.slice(0, -cyWithExt.length) + return this.getFinalFileName(dir, name, cyWithExt, path.join(dir, `${name}${cyWithExt}`)) + } + + private async getFinalFileName (dir: string, name: string, cyWithExt: string, fileToTry: string) { // At this point, for a filePath of `/foo/bar/baz.cy.js` // - name = `baz` // - cyWithExt = `.cy.js` - let fileToTry = this.ctx.path.join(dir, `${name}${cyWithExt}`) + let finalFileName = fileToTry let i = 0 - while (await this.fileExists(fileToTry)) { - fileToTry = this.ctx.path.join( + while (await this.fileExists(finalFileName)) { + finalFileName = path.join( dir, `${name}-copy-${++i}${cyWithExt}`, ) } - return this.ctx.path.parse(fileToTry).base + return path.parse(finalFileName).base } private async fileExists (absolute: string) { try { - await this.ctx.fs.access(absolute, this.ctx.fs.constants.F_OK) + await fs.access(absolute, fs.constants.F_OK) return true } catch (e) { diff --git a/packages/data-context/src/codegen/templates.ts b/packages/data-context/src/codegen/templates.ts index b2d03e94e8..58f7a03bbe 100644 --- a/packages/data-context/src/codegen/templates.ts +++ b/packages/data-context/src/codegen/templates.ts @@ -4,7 +4,8 @@ import cypressEx from '@packages/example' const getPath = (dir: string) => path.join(__dirname, dir) export default { - component: getPath('./templates/empty-component'), + vueComponent: getPath('./templates/vue-component'), + componentEmpty: getPath('./templates/empty-component'), e2e: getPath('./templates/empty-e2e'), scaffoldIntegration: cypressEx.getPathToE2E(), } as const diff --git a/packages/data-context/src/codegen/templates/vue-component/vue-component.ejs b/packages/data-context/src/codegen/templates/vue-component/vue-component.ejs new file mode 100644 index 0000000000..48335fd9b5 --- /dev/null +++ b/packages/data-context/src/codegen/templates/vue-component/vue-component.ejs @@ -0,0 +1,12 @@ +--- +fileName: <%= fileName %> +--- + +import <%- componentName %> from "<%- componentPath %>" + +describe('<<%=componentName%> />', () => { + it('renders', () => { + // see: https://test-utils.vuejs.org/guide/ + cy.mount(<%- componentName %>) + }) +}) \ No newline at end of file diff --git a/packages/data-context/src/sources/ProjectDataSource.ts b/packages/data-context/src/sources/ProjectDataSource.ts index 7ed4e79bb2..441dc4c17e 100644 --- a/packages/data-context/src/sources/ProjectDataSource.ts +++ b/packages/data-context/src/sources/ProjectDataSource.ts @@ -478,6 +478,18 @@ export class ProjectDataSource { return preferences[projectTitle] ?? null } + async getCodeGenGlobs () { + assert(this.ctx.currentProject, `Cannot find glob without currentProject.`) + + const looseComponentGlob = '*.{js,jsx,ts,tsx,vue}' + + const framework = this.ctx.actions.project.getWizardFrameworkFromConfig() + + return { + component: framework?.glob ?? looseComponentGlob, + } + } + async getResolvedConfigFields (): Promise { const config = this.ctx.lifecycleManager.loadedFullConfig?.resolved ?? {} diff --git a/packages/data-context/test/unit/codegen/code-generator.spec.ts b/packages/data-context/test/unit/codegen/code-generator.spec.ts index 50ce32cfde..dcf1b1a8ae 100644 --- a/packages/data-context/test/unit/codegen/code-generator.spec.ts +++ b/packages/data-context/test/unit/codegen/code-generator.spec.ts @@ -10,6 +10,7 @@ import { import { SpecOptions } from '../../../src/codegen/spec-options' import templates from '../../../src/codegen/templates' import { createTestDataContext } from '../helper' +import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config' const tmpPath = path.join(__dirname, 'tmp/test-code-gen') @@ -144,22 +145,15 @@ describe('code-generator', () => { expect(() => babelParse(fileContent)).not.throw() }) - it('should generate from component template', async () => { + it('should generate from empty component template', async () => { const fileName = 'Button.tsx' const target = path.join(tmpPath, 'component') const fileAbsolute = path.join(target, fileName) const action: Action = { - templateDir: templates.component, + templateDir: templates.componentEmpty, target, } const codeGenArgs = { - imports: [ - 'import { mount } from "@cypress/react"', - 'import Button from "./Button"', - ], - componentName: 'Button', - docsLink: '// see: https://on.cypress.io/component-testing', - mount: 'mount(