feat: Create spec from Vue component (#22898)

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
Co-authored-by: Zachary Williams <ZachJW34@gmail.com>
This commit is contained in:
Adam Stone
2022-08-09 11:50:27 -04:00
committed by GitHub
parent 453f53f070
commit 7ef6f972aa
52 changed files with 6505 additions and 81 deletions
+1
View File
@@ -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
+112
View File
@@ -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('<HelloWorld ... />').should('be.visible')
})
})
context('project with custom spec pattern', () => {
beforeEach(() => {
cy.scaffoldProject('no-specs-custom-pattern')
+4 -2
View File
@@ -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')"
/>
</template>
@@ -21,6 +22,7 @@ defineProps<{
header: string
description: string
disabled?: boolean
badgeText?: string
}>()
const emits = defineEmits<{
+6 -2
View File
@@ -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)"
/>
</div>
@@ -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
}
}
}
`
@@ -15,6 +15,11 @@ describe('<CreateSpecModal />', () => {
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('<CreateSpecModal />', () => {
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',
+4 -2
View File
@@ -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)
@@ -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
+2 -2
View File
@@ -16,7 +16,7 @@ describe('<NoSpecsPage />', { 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('<NoSpecsPage />', { 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',
+6 -2
View File
@@ -6,13 +6,13 @@
<div class="m-x-auto max-w-600px">
<h1
data-cy="create-spec-page-title"
class="text-gray-900 mb-12px text-32px"
class="mb-12px text-gray-900 text-32px"
>
{{ props.title }}
</h1>
<p
data-cy="create-spec-page-description"
class="leading-normal text-gray-600 mb-32px text-18px"
class="leading-normal mb-32px text-gray-600 text-18px"
>
<i18n-t
scope="global"
@@ -63,6 +63,10 @@ fragment NoSpecsPage on Query {
...ChooseExternalEditor
currentProject {
id
codeGenGlobs {
id
component
}
currentTestingType
configFileAbsolutePath
...CustomPatternNoSpecContent
@@ -93,7 +93,7 @@
query: {
file: result.file.relative?.replace(/\\/g, '/')
},
params: props.type === 'component'
params: props.type === 'component' || props.type === 'componentEmpty'
? {
shouldShowTroubleRenderingAlert: true
}
@@ -141,7 +141,7 @@ import PlusButtonIcon from '~icons/cy/add-large_x16.svg'
const props = defineProps<{
title: string
gql: EmptyGeneratorFragment
type: 'e2e' | 'component'
type: 'e2e' | 'component' | 'componentEmpty'
specFileName: string
erroredCodegenCandidate?: string
/** is there any other generator available when clicking "Back" */
@@ -0,0 +1,18 @@
import { filters } from '../GeneratorsCommon'
import ComponentGeneratorStepOne from './ComponentGeneratorStepOne.vue'
import type { SpecGenerator } from '../types'
import ComponentGeneratorCard from './ComponentGeneratorCard.vue'
export const ComponentGenerator: SpecGenerator = {
card: ComponentGeneratorCard,
entry: ComponentGeneratorStepOne,
show: (currentProject, isDefaultSpecPattern) => {
if (!isDefaultSpecPattern) {
return false
}
return currentProject?.codeGenGlobs?.component === '*.vue'
},
matches: filters.matchesCT,
id: 'component',
}
@@ -0,0 +1,20 @@
<template>
<CreateSpecCard
:disabled="disabled"
:header="t('createSpec.component.importFromComponent.header')"
:description="t('createSpec.component.importFromComponent.description')"
:icon="DocumentCodeIcon"
:badge-text="t('versions.new')"
/>
</template>
<script lang="ts" setup>
import CreateSpecCard from '../../CreateSpecCard.vue'
import { useI18n } from '@cy/i18n'
import DocumentCodeIcon from '~icons/cy/document-code_x48'
const { t } = useI18n()
defineProps<{
disabled?: boolean
}>()
</script>
@@ -0,0 +1,197 @@
<template>
<div class="flex flex-col flex-grow justify-between">
<template v-if="generatedSpecError">
<EmptyGenerator
:gql="generateSpecFromSource.currentProject"
title=""
type="component"
:other-generators="false"
:spec-file-name="generatedSpecError.fileName"
:errored-codegen-candidate="generatedSpecError.erroredCodegenCandidate"
@restart="cancelSpecNameCreation"
@updateTitle="(value) => emits('update:title', value)"
/>
</template>
<template v-else>
<div class="flex-grow">
<div
v-if="mutation.fetching.value"
class="mt-48px w-full inline-flex items-center justify-center"
>
<i-cy-loading_x16 class="h-48px mr-12px animate-spin w-48px" />
<p class="text-lg">
Loading
</p>
</div>
<FileChooser
v-else-if="!result"
v-model:extensionPattern="extensionPattern"
:files="allFiles"
:loading="query.fetching.value"
@selectFile="makeSpec"
/>
<GeneratorSuccess
v-else
:file="result.file"
/>
</div>
<div>
<StandardModalFooter
v-if="result"
class="flex gap-16px items-center"
>
<router-link
class="outline-none"
:to="{ name: 'SpecRunner', query: { file: result.file.relative?.replace(/\\/g, '/') }, params: { shouldShowTroubleRenderingAlert: true } }
"
>
<Button
size="lg"
:prefix-icon="TestResultsIcon"
prefix-icon-class="w-16px h-16px icon-dark-white"
@click="emits('close')"
>
{{ t('createSpec.successPage.runSpecButton') }}
</Button>
</router-link>
<Button
size="lg"
:prefix-icon="PlusButtonIcon"
prefix-icon-class="w-16px h-16px icon-dark-gray-500"
variant="outline"
@click="emits('restart')"
>
{{ t('createSpec.successPage.createAnotherSpecButton') }}
</Button>
</StandardModalFooter>
<div
v-else
class="bg-white rounded-b h-24px bottom-0 left-0 absolute ghost-div"
/>
</div>
</template>
</div>
</template>
<script setup lang="ts">
import { useVModels, whenever } from '@vueuse/core'
import { useI18n } from '@cy/i18n'
import FileChooser from '../FileChooser.vue'
import GeneratorSuccess from '../GeneratorSuccess.vue'
import { computed, ref } from 'vue'
import { gql, useQuery, useMutation } from '@urql/vue'
import type { ComponentGeneratorStepOne_CodeGenGlobFragment, GeneratorSuccessFileFragment } from '../../../generated/graphql'
import { ComponentGeneratorStepOneDocument, ComponentGeneratorStepOne_GenerateSpecDocument } from '../../../generated/graphql'
import StandardModalFooter from '@cy/components/StandardModalFooter.vue'
import Button from '@cy/components/Button.vue'
import PlusButtonIcon from '~icons/cy/add-large_x16.svg'
import TestResultsIcon from '~icons/cy/test-results_x24.svg'
import EmptyGenerator from '../EmptyGenerator.vue'
const props = defineProps<{
title: string
gql: ComponentGeneratorStepOne_CodeGenGlobFragment
}>()
const { t } = useI18n()
const emits = defineEmits<{
(event: 'update:title', value: string): void
(event: 'update:description', value: string): void
(event: 'restart'): void
(event: 'close'): void
}>()
const { title } = useVModels(props, emits)
title.value = t('createSpec.component.importFromComponent.chooseAComponentHeader')
gql`
fragment ComponentGeneratorStepOne_codeGenGlob on CurrentProject {
id
codeGenGlobs {
id
component
}
}
`
gql`
query ComponentGeneratorStepOne($glob: String!) {
currentProject {
id
codeGenCandidates(glob: $glob) {
id
fileName
fileExtension
absolute
relative
baseName
}
# Add the specs, so we can keep the list up to date with the cache
specs {
id
...SpecNode_InlineSpecList
}
}
}
`
gql`
mutation ComponentGeneratorStepOne_generateSpec($codeGenCandidate: String!, $type: CodeGenType!) {
generateSpecFromSource(codeGenCandidate: $codeGenCandidate, type: $type) {
...GeneratorSuccess
currentProject {
id
...EmptyGenerator
}
generatedSpecResult {
... on GeneratedSpecError {
fileName
erroredCodegenCandidate
}
}
}
}`
const mutation = useMutation(ComponentGeneratorStepOne_GenerateSpecDocument)
const extensionPattern = ref(props.gql.codeGenGlobs.component)
const query = useQuery({
query: ComponentGeneratorStepOneDocument,
// @ts-ignore
variables: { glob: extensionPattern },
})
const allFiles = computed((): any => {
if (query.data.value?.currentProject?.codeGenCandidates) {
return query.data.value.currentProject?.codeGenCandidates
}
return []
})
const result = ref<GeneratorSuccessFileFragment | null>(null)
const generatedSpecError = ref()
const generateSpecFromSource = ref()
whenever(result, () => {
title.value = t('createSpec.successPage.header')
})
whenever(generatedSpecError, () => {
title.value = t('createSpec.component.importEmptySpec.header')
})
const makeSpec = async (file) => {
const { data } = await mutation.executeMutation({
codeGenCandidate: file.absolute,
type: 'component',
})
generateSpecFromSource.value = data?.generateSpecFromSource
result.value = data?.generateSpecFromSource?.generatedSpecResult?.__typename === 'ScaffoldedFile' ? data?.generateSpecFromSource?.generatedSpecResult : null
generatedSpecError.value = data?.generateSpecFromSource?.generatedSpecResult?.__typename === 'GeneratedSpecError' ? data?.generateSpecFromSource?.generatedSpecResult : null
}
const cancelSpecNameCreation = () => {
generatedSpecError.value = null
}
</script>
<style scoped>
.ghost-div {
width: calc(100% - 24px);
}
</style>
@@ -0,0 +1 @@
export * from './ComponentGenerator'
@@ -6,6 +6,6 @@ export const EmptyGenerator: SpecGenerator = {
card: EmptyGeneratorCard,
entry: EmptyGeneratorCardStepOne,
matches: () => true,
disabled: () => false,
show: () => true,
id: 'empty',
}
@@ -15,7 +15,7 @@ import DocumentCodeIcon from '~icons/cy/document-code_x48'
const { t } = useI18n()
defineProps<{
disabled: boolean
disabled?: boolean
}>()
</script>
+5 -2
View File
@@ -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<GeneratorId, SpecGenerator>
@@ -7,6 +7,6 @@ export const ScaffoldGenerator: SpecGenerator = {
card: ScaffoldGeneratorCard,
entry: ScaffoldGeneratorStepOne,
matches: filters.matchesE2E,
disabled: () => { },
show: () => true,
id: 'scaffold',
}
@@ -15,7 +15,7 @@ import BoxOpenIcon from '~icons/cy/box-open_x48'
const { t } = useI18n()
defineProps<{
disabled: boolean
disabled?: boolean
}>()
</script>
+5 -2
View File
@@ -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
}
@@ -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)
}
}
+103 -12
View File
@@ -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<ComponentExtension> {
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) {
@@ -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
@@ -0,0 +1,12 @@
---
fileName: <%= fileName %>
---
import <%- componentName %> from "<%- componentPath %>"
describe('<<%=componentName%> />', () => {
it('renders', () => {
// see: https://test-utils.vuejs.org/guide/
cy.mount(<%- componentName %>)
})
})
@@ -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<ResolvedFromConfig[]> {
const config = this.ctx.lifecycleManager.loadedFullConfig?.resolved ?? {}
@@ -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(<Button />)',
fileName,
}
@@ -190,6 +184,49 @@ describe('code-generator', () => {
expect(() => babelParse(fileContent)).not.throw()
})
it('should generate from Vue component template', async () => {
const fileName = 'MyComponent.vue'
const target = path.join(tmpPath, 'component')
const fileAbsolute = path.join(target, fileName)
const action: Action = {
templateDir: templates.vueComponent,
target,
}
const codeGenArgs = {
componentName: 'MyComponent',
componentPath: 'path/to/component',
fileName,
}
const codeGenResults = await codeGenerator(action, codeGenArgs)
const expected: CodeGenResults = {
files: [
{
type: 'text',
status: 'add',
file: fileAbsolute,
content: dedent`import ${codeGenArgs.componentName} from "${codeGenArgs.componentPath}"
describe('<${codeGenArgs.componentName} />', () => {
it('renders', () => {
// see: https://test-utils.vuejs.org/guide/
cy.mount(${codeGenArgs.componentName})
})
})`,
},
],
failed: [],
}
expect(codeGenResults).deep.eq(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
})
it('should generate from scaffoldIntegration', async () => {
const target = path.join(tmpPath, 'scaffold-integration')
const action: Action = {
@@ -212,17 +249,19 @@ describe('code-generator', () => {
}
})
it('should generate from react component', async () => {
it('should generate empty test from react component', async () => {
const target = path.join(tmpPath, 'react-component')
const action: Action = {
templateDir: templates.component,
templateDir: templates.componentEmpty,
target,
}
const newSpecCodeGenOptions = new SpecOptions(ctx, {
const newSpecCodeGenOptions = new SpecOptions({
codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'),
codeGenType: 'component',
specFileExtension: '.cy',
framework: WIZARD_FRAMEWORKS[1],
isDefaultSpecPattern: true,
})
let codeGenOptions = await newSpecCodeGenOptions.getCodeGenOptions()
@@ -231,24 +270,4 @@ describe('code-generator', () => {
expect(() => babelParse(codeGenResult.files[0].content)).not.throw()
})
it('should generate from vue component', async () => {
const target = path.join(tmpPath, 'vue-component')
const action: Action = {
templateDir: templates.component,
target,
}
const newSpecCodeGenOptions = new SpecOptions(ctx, {
codeGenPath: path.join(__dirname, 'files', 'vue', 'Button.vue'),
codeGenType: 'component',
specFileExtension: '.cy',
})
let codeGenOptions = await newSpecCodeGenOptions.getCodeGenOptions()
const codeGenResult = await codeGenerator(action, codeGenOptions)
expect(() => codeGenResult.files[0].content).not.throw()
})
})
@@ -1,3 +1,4 @@
import { WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
import { expect } from 'chai'
import fs from 'fs-extra'
import path from 'path'
@@ -27,10 +28,11 @@ describe('spec-options', () => {
context('unique file names', () => {
for (const specExtension of expectedSpecExtensions) {
it(`generates options for name names with extension ${specExtension}`, async () => {
const testSpecOptions = new SpecOptions(ctx, {
it(`generates options for names with extension ${specExtension}`, async () => {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName${specExtension}.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
const result = await testSpecOptions.getCodeGenOptions()
@@ -41,9 +43,10 @@ describe('spec-options', () => {
}
it('generates options for file name without spec extension', async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
const result = await testSpecOptions.getCodeGenOptions()
@@ -53,9 +56,10 @@ describe('spec-options', () => {
})
it('generates options for file name with multiple extensions', async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName.foo.bar.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
const result = await testSpecOptions.getCodeGenOptions()
@@ -65,9 +69,11 @@ describe('spec-options', () => {
})
it('generates options with given codeGenType', async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName.js`,
codeGenType: 'component',
isDefaultSpecPattern: true,
framework: WIZARD_FRAMEWORKS[1],
})
const result = await testSpecOptions.getCodeGenOptions()
@@ -79,9 +85,10 @@ describe('spec-options', () => {
context('duplicate files names', () => {
for (const specExtension of expectedSpecExtensions) {
it(`generates options for file name with extension ${specExtension}`, async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName${specExtension}.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
await fs.outputFile(`${tmpPath}/TestName${specExtension}.js`, '// foo')
@@ -102,9 +109,10 @@ describe('spec-options', () => {
}
it('generates options for file name without spec extension', async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
await fs.outputFile(`${tmpPath}/TestName.js`, '// foo')
@@ -124,9 +132,10 @@ describe('spec-options', () => {
})
it('generates options for file name with multiple extensions', async () => {
const testSpecOptions = new SpecOptions(ctx, {
const testSpecOptions = new SpecOptions({
codeGenPath: `${tmpPath}/TestName.foo.bar.js`,
codeGenType: 'e2e',
isDefaultSpecPattern: true,
})
await fs.outputFile(`${tmpPath}/TestName.foo.bar.js`, '// foo')
@@ -45,6 +45,11 @@ export const createTestCurrentProject = (title: string, currentProject: Partial<
],
config,
cloudProject: CloudProjectStubs.componentProject,
codeGenGlobs: {
id: 'super-unique-id',
__typename: 'CodeGenGlobs',
component: '**/*.vue',
},
activeBrowser: stubBrowsers[0],
browsers: stubBrowsers,
isDefaultSpecPattern: true,
@@ -11,11 +11,11 @@
@click="!disabled && emits('click')"
>
<div
v-if="title === t('testingType.component.name')"
v-if="badgeText"
class="top-0 right-0 text-teal-600 ribbon absolute"
aria-hidden="true"
>
{{ t('versions.beta') }}
{{ badgeText }}
</div>
<div
class="mx-auto children:transition-all children:duration-300"
@@ -77,9 +77,11 @@ const props = withDefaults(defineProps<{
variant: 'indigo' | 'jade'
iconSize: 64 | 48
disabled?: boolean
badgeText?: string
}>(), {
disabled: false,
hoverIcon: undefined,
badgeText: '',
})
const classMap = {
@@ -15,6 +15,7 @@
:icon-size="64"
:disabled="tt.status === 'disabled'"
variant="jade"
:badge-text="tt.badgeText"
@click="emits('pick', tt.key, currentTestingType)"
@keyup.enter="emits('pick', tt.key, currentTestingType)"
@keyup.space="emits('pick', tt.key, currentTestingType)"
@@ -102,6 +103,7 @@ const testingTypes = computed(() => {
icon: IconE2E,
iconSolid: IconE2ESolid,
status: e2eStatus.value,
badgeText: '',
},
{
key: 'component',
@@ -110,6 +112,7 @@ const testingTypes = computed(() => {
icon: IconComponent,
iconSolid: IconComponentSolid,
status: componentStatus.value,
badgeText: t('versions.beta'),
},
] as const
})
@@ -105,7 +105,7 @@
"component": {
"importFromComponent": {
"header": "Create from component",
"description": "We'll generate an empty spec file which can be used to import and test any component in this project.",
"description": "Generate a basic component test for any of the components in this project.",
"chooseAComponentHeader": "Choose a component"
},
"importEmptySpec": {
@@ -780,6 +780,7 @@
},
"versions": {
"alpha": "Alpha",
"beta": "Beta"
"beta": "Beta",
"new": "New"
}
}
+44
View File
@@ -1,6 +1,16 @@
### This file was generated by Nexus Schema
### Do not make changes to this file directly
"""
Feature not available for subscription
"""
type CloudFeatureNotEnabled {
"""
an error message
"""
message: String!
}
"""
Represents a pollable status for clients to know when refetching data is required.
"""
@@ -211,6 +221,17 @@ type CloudProjectSpec implements Node {
"""
fromBranch: String!
): Float
flakyStatus(
"""
The number of runs to consider when counting flaky runs.
"""
flakyRunsWindow: Int!
"""
The branch to measure average duration against. This will fallback to the closest branch with data.
"""
fromBranch: String!
): CloudProjectSpecFlakyResult
"""
Globally unique identifier representing a concrete GraphQL ObjectType
@@ -264,6 +285,29 @@ type CloudProjectSpec implements Node {
): CloudSpecRunConnection
}
union CloudProjectSpecFlakyResult =
CloudFeatureNotEnabled
| CloudProjectSpecFlakyStatus
type CloudProjectSpecFlakyStatus {
"""
URL linking to the flaky data in the Cypress dashboard for this spec
"""
dashboardUrl: String
"""
Number of flaky runs from the considered runs
"""
flakyRuns: Int
flakyRunsWindow: Int
"""
The last flaky run occurrence, interpreted as "n runs ago" - ex: a value of 5 means a flaky run last occurred 5 runs ago
"""
lastFlaky: Int
severity: String
}
"""
Unable to find cloud spec in project
"""
+42
View File
@@ -65,6 +65,12 @@ type CachedUser implements Node {
id: ID!
}
"""Feature not available for subscription"""
type CloudFeatureNotEnabled {
"""an error message"""
message: String!
}
"""
Represents a pollable status for clients to know when refetching data is required.
"""
@@ -208,6 +214,15 @@ type CloudProjectSpec implements Node {
"""
fromBranch: String!
): Float
flakyStatus(
"""The number of runs to consider when counting flaky runs."""
flakyRunsWindow: Int!
"""
The branch to measure average duration against. This will fallback to the closest branch with data.
"""
fromBranch: String!
): CloudProjectSpecFlakyResult
"""Globally unique identifier representing a concrete GraphQL ObjectType"""
id: ID!
@@ -245,6 +260,23 @@ type CloudProjectSpec implements Node {
): CloudSpecRunConnection
}
union CloudProjectSpecFlakyResult = CloudFeatureNotEnabled | CloudProjectSpecFlakyStatus
type CloudProjectSpecFlakyStatus {
"""URL linking to the flaky data in the Cypress dashboard for this spec"""
dashboardUrl: String
"""Number of flaky runs from the considered runs"""
flakyRuns: Int
flakyRunsWindow: Int
"""
The last flaky run occurrence, interpreted as "n runs ago" - ex: a value of 5 means a flaky run last occurred 5 runs ago
"""
lastFlaky: Int
severity: String
}
"""Unable to find cloud spec in project"""
type CloudProjectSpecNotFound {
"""an error message"""
@@ -502,8 +534,17 @@ type CodeFrame {
line: Int
}
"""Glob patterns for detecting files for code gen."""
type CodeGenGlobs implements Node {
component: String!
"""Relay style Node ID field for the CodeGenGlobs field"""
id: ID!
}
enum CodeGenType {
component
componentEmpty
e2e
scaffoldIntegration
}
@@ -543,6 +584,7 @@ type CurrentProject implements Node & ProjectLike {
"""List of all code generation candidates stories"""
codeGenCandidates(glob: String!): [FileParts]
codeGenGlobs: CodeGenGlobs!
"""Project configuration"""
config: JSON!
@@ -2,5 +2,5 @@ import { enumType } from 'nexus'
export const CodeGenTypeEnum = enumType({
name: 'CodeGenType',
members: ['component', 'e2e', 'scaffoldIntegration'],
members: ['component', 'componentEmpty', 'e2e', 'scaffoldIntegration'],
})
@@ -0,0 +1,10 @@
import { objectType } from 'nexus'
export const CodeGenGlobs = objectType({
name: 'CodeGenGlobs',
description: 'Glob patterns for detecting files for code gen.',
node: 'component',
definition (t) {
t.nonNull.string('component')
},
})
@@ -4,6 +4,7 @@ import path from 'path'
import { BrowserStatusEnum, FileExtensionEnum } from '..'
import { TestingTypeEnum } from '../enumTypes/gql-WizardEnums'
import { Browser } from './gql-Browser'
import { CodeGenGlobs } from './gql-CodeGenGlobs'
import { FileParts } from './gql-FileParts'
import { ProjectPreferences } from './gql-ProjectPreferences'
import { Spec } from './gql-Spec'
@@ -193,6 +194,11 @@ export const CurrentProject = objectType({
},
})
t.nonNull.field('codeGenGlobs', {
type: CodeGenGlobs,
resolve: (src, args, ctx) => ctx.project.getCodeGenGlobs(),
})
t.list.field('codeGenCandidates', {
type: FileParts,
description: 'List of all code generation candidates stories',
@@ -5,6 +5,7 @@ export * from './gql-AuthState'
export * from './gql-Browser'
export * from './gql-CachedUser'
export * from './gql-CodeFrame'
export * from './gql-CodeGenGlobs'
export * from './gql-CurrentProject'
export * from './gql-DevState'
export * from './gql-Editor'
@@ -78,6 +78,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'react',
glob: '*.{js,jsx,tsx}',
mountModule: 'cypress/react',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -97,6 +98,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'vue',
glob: '*.vue',
mountModule: 'cypress/vue2',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -116,6 +118,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'vue',
glob: '*.vue',
mountModule: 'cypress/vue',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -135,6 +138,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'react',
glob: '*.{js,jsx,tsx}',
mountModule: 'cypress/react',
supportStatus: 'alpha',
/**
@@ -160,6 +164,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'vue',
glob: '*.vue',
mountModule: 'cypress/vue2',
supportStatus: 'alpha',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -178,6 +183,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'vue',
glob: '*.vue',
mountModule: 'cypress/vue2',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -196,6 +202,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'vue',
glob: '*.vue',
mountModule: 'cypress/vue',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -215,6 +222,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'react',
glob: '*.{js,jsx,tsx}',
mountModule: 'cypress/react',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -236,6 +244,7 @@ export const WIZARD_FRAMEWORKS = [
])
},
codeGenFramework: 'angular',
glob: '*.component.ts',
mountModule: 'cypress/angular',
supportStatus: 'full',
componentIndexHtml: componentIndexHtmlGenerator(),
@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
@@ -0,0 +1,8 @@
module.exports = {
component: {
devServer: {
framework: 'vue-cli',
bundler: 'webpack',
},
}
}
@@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>
@@ -0,0 +1,27 @@
// ***********************************************************
// This example support/component.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
import { mount } from 'cypress/vue2'
Cypress.Commands.add('mount', mount)
// Example use:
// cy.mount(MyComponent)
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
}
}
@@ -0,0 +1,26 @@
{
"name": "no-specs-vue-2",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.8.3",
"vue": "^2.6.14"
},
"devDependencies": {
"@babel/core": "^7.12.16",
"@babel/eslint-parser": "^7.12.16",
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"vue-template-compiler": "^2.6.14"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
@@ -0,0 +1,28 @@
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
@@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
@@ -0,0 +1,8 @@
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
@@ -0,0 +1,4 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true
})
File diff suppressed because it is too large Load Diff