mirror of
https://github.com/cypress-io/cypress.git
synced 2026-05-25 01:49:06 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<{
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user