fix(launchpad): scaffold correct config file (#20372)

This commit is contained in:
Zachary Williams
2022-02-25 17:29:17 -06:00
committed by GitHub
parent 2ba40a0d4b
commit 7f8a810e3d
281 changed files with 49580 additions and 737 deletions

View File

@@ -63,3 +63,24 @@ npm/cypress-schematic/src/**/*.js
/npm/create-cypress-tests/**/*.template.*
packages/data-context/test/unit/codegen/files
# community templates we test against, no need to lint
system-tests/projects/create-react-app-configured/**/*
system-tests/projects/create-react-app-unconfigured/**/*
system-tests/projects/vueclivue2-unconfigured/**/*
system-tests/projects/vueclivue2-configured/**/*
system-tests/projects/vueclivue3-unconfigured/**/*
system-tests/projects/vueclivue3-configured/**/*
system-tests/projects/vue3-vite-ts-unconfigured/**/*
system-tests/projects/vue3-vite-ts-configured/**/*
system-tests/projects/nextjs-unconfigured/**/*
system-tests/projects/nextjs-configured/**/*
system-tests/projects/nuxtjs-vue2-unconfigured/**/*
system-tests/projects/nuxtjs-vue2-configured/**/*
system-tests/projects/react-app-webpack-5-unconfigured/**/*

View File

@@ -2,7 +2,8 @@
"prefix": "/* eslint-disable padding-line-between-statements */",
"paths": [
"packages/graphql/src/**/*",
"packages/data-context/src/**/*"
"packages/data-context/src/**/*",
"packages/scaffold-config/src/**"
],
"ignore": [
"packages/data-context/src/gen",

View File

@@ -561,8 +561,8 @@ describe('App: Index', () => {
it('shows input for file extension filter', () => {
cy.get('@CreateFromComponentDialog').within(() => {
cy.log('testing builds')
cy.findByTestId('file-match-indicator').should('contain', '2 Matches')
cy.findByRole('button', { name: '*.{jsx,tsx}' }).click()
cy.findByTestId('file-match-indicator').should('contain', '4 Matches')
cy.findByRole('button', { name: '*.{js,jsx,tsx}' }).click()
cy.findByPlaceholderText(defaultMessages.components.fileSearch.byExtensionInput)
.as('ExtensionInput')
.clear()
@@ -572,9 +572,9 @@ describe('App: Index', () => {
cy.findByTestId('no-results-clear').click()
cy.get('@ExtensionInput').should('have.value', '*.{jsx,tsx}')
cy.get('@ExtensionInput').should('have.value', '*.{js,jsx,tsx}')
cy.findByTestId('file-match-indicator').should('contain', '2 Matches')
cy.findByTestId('file-match-indicator').should('contain', '4 Matches')
})
})
@@ -583,7 +583,7 @@ describe('App: Index', () => {
cy.findByLabelText('file-name-input').as('FileNameInput')
.should('have.value', '')
cy.findByTestId('file-match-indicator').should('contain', '2 Matches')
cy.findByTestId('file-match-indicator').should('contain', '4 Matches')
cy.get('@FileNameInput')
.type('foobar')
@@ -592,18 +592,18 @@ describe('App: Index', () => {
cy.findByTestId('no-results-clear').click()
cy.findByTestId('file-match-indicator').should('contain', '2 Matches')
cy.findByTestId('file-match-indicator').should('contain', '4 Matches')
cy.get('@FileNameInput')
.type('App.jsx')
cy.findByTestId('file-match-indicator').should('contain', '1 of 2 Matches')
cy.findByTestId('file-match-indicator').should('contain', '1 of 4 Matches')
})
})
it('shows success modal when spec is created from component', () => {
cy.get('@CreateFromComponentDialog').within(() => {
cy.findAllByTestId('file-list-row').eq(0).as('NewSpecFile')
cy.findAllByTestId('file-list-row').contains('App.jsx').as('NewSpecFile')
// TODO: assert visibility of secondary text on hover/focus when
// item is made keyboard accessible
@@ -732,7 +732,7 @@ describe('App: Index', () => {
.and('contain', defaultMessages.createSpec.component.importFromComponent.description).click()
})
cy.get('[data-cy=file-list-row]').first().click()
cy.get('[data-cy=file-list-row]').contains('App.jsx').click()
cy.get('input').invoke('val').should('eq', getPathForPlatform('src/App.cy.jsx'))
cy.contains(defaultMessages.createSpec.component.importEmptySpec.header)
@@ -750,7 +750,7 @@ describe('App: Index', () => {
.and('contain', defaultMessages.createSpec.component.importFromComponent.description).click()
})
cy.get('[data-cy=file-list-row]').first().click()
cy.get('[data-cy=file-list-row]').contains('App.jsx').click()
cy.get('input').invoke('val').should('eq', getPathForPlatform('src/App.cy.jsx'))
cy.contains(defaultMessages.createSpec.component.importEmptySpec.header)
@@ -824,11 +824,11 @@ describe('App: Index', () => {
})
cy.contains('Create from component').click()
const componentGlob = '*.{jsx,tsx}'
const componentGlob = '*.{js,jsx,tsx}'
cy.findByTestId('file-match-button').contains(componentGlob)
cy.percySnapshot('Component Generator')
checkCodeGenCandidates(['App.cy.jsx', 'App.jsx', 'index.jsx', 'Button.jsx', 'Button.stories.jsx'])
checkCodeGenCandidates(['cypress.config.js', 'App.cy.jsx', 'App.jsx', 'index.jsx', 'support.js', 'Button.jsx', 'Button.stories.jsx'])
cy.intercept('query-ComponentGeneratorStepOne').as('code-gen-candidates')
cy.findByTestId('file-match-button').click()

View File

@@ -1,4 +1,5 @@
import path from 'path'
import assert from 'assert'
import type { DataContext } from '..'
import {
cleanUpIntegrationFolder,
@@ -127,4 +128,24 @@ export class MigrationActions {
throw Error(`Expected ${actual} to equal ${expected}`)
}
}
async assertSuccessfulConfigScaffold (configFile: `cypress.config.${'js'|'ts'}`) {
assert(this.ctx.currentProject)
// we assert the generated configuration file against one from a project that has
// been verified to run correctly.
// each project has an `unconfigured` and `configured` variant in `system-tests/projects`
// for example vueclivue2-configured and vueclivue2-unconfigured.
// after setting the project up with the launchpad, the two projects should contain the same files.
const configuredProject = this.ctx.project.projectTitle(this.ctx.currentProject).replace('unconfigured', 'configured')
const expectedProjectConfig = path.join(__dirname, '..', '..', '..', '..', 'system-tests', 'projects', configuredProject, configFile)
const actual = formatConfig(await this.ctx.actions.file.readFileInProject(configFile))
const expected = formatConfig(await this.ctx.fs.readFile(expectedProjectConfig, 'utf8'))
if (actual !== expected) {
throw Error(`Expected ${actual} to equal ${expected}`)
}
}
}

View File

@@ -1,8 +1,10 @@
import type { CodeLanguageEnum, NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
import { Bundler, CodeLanguage, CODE_LANGUAGES, FrontendFramework, FRONTEND_FRAMEWORKS } from '@packages/types'
import { CodeLanguage, CODE_LANGUAGES } from '@packages/types'
import { Bundler, FrontendFramework, FRONTEND_FRAMEWORKS, detect } from '@packages/scaffold-config'
import assert from 'assert'
import dedent from 'dedent'
import path from 'path'
import fs from 'fs-extra'
import Debug from 'debug'
const debug = Debug('cypress:data-context:wizard-actions')
@@ -12,7 +14,6 @@ import type { DataContext } from '..'
interface WizardGetCodeComponent {
chosenLanguage: CodeLanguage
chosenFramework: FrontendFramework
chosenBundler: Bundler
}
export class WizardActions {
@@ -28,28 +29,33 @@ export class WizardActions {
return this.ctx.wizardData
}
setFramework (framework: NexusGenEnums['FrontendFrameworkEnum'] | null) {
const prevFramework = this.ctx.coreData.wizard.chosenFramework || ''
setFramework (framework: typeof FRONTEND_FRAMEWORKS[number]['type'] | null): void {
const next = FRONTEND_FRAMEWORKS.find((x) => x.type === framework)
this.ctx.coreData.wizard.chosenFramework = framework
if (framework !== 'react' && framework !== 'vue') {
return this.setBundler('webpack')
if (next?.supportedBundlers?.length === 1) {
this.setBundler(next?.supportedBundlers?.[0].type)
return
}
const { chosenBundler } = this.ctx.coreData.wizard
// if the previous bundler was incompatible with the
// new framework, we need to reset it
if ((chosenBundler && !this.ctx.wizard.chosenFramework?.supportedBundlers.includes(chosenBundler))
|| !['react', 'vue'].includes(prevFramework)) {
return this.setBundler(null)
}
// new framework that was selected, we need to reset it
const doesNotSupportChosenBundler = (chosenBundler && !new Set(
this.ctx.wizard.chosenFramework?.supportedBundlers.map((x) => x.type) || [],
).has(chosenBundler)) ?? false
return
const prevFramework = this.ctx.coreData.wizard.chosenFramework || ''
if (doesNotSupportChosenBundler || !['react', 'vue'].includes(prevFramework)) {
this.setBundler(null)
}
}
setBundler (bundler: NexusGenEnums['SupportedBundlers'] | null) {
setBundler (bundler: Bundler | null) {
this.ctx.coreData.wizard.chosenBundler = bundler
return this.data
@@ -83,76 +89,40 @@ export class WizardActions {
}
async initialize () {
if (this.ctx.currentProject) {
this.data.detectedFramework = null
this.data.detectedBundler = null
this.data.detectedLanguage = null
await this.detectLanguage()
debug('detectedLanguage %s', this.data.detectedLanguage)
this.data.chosenLanguage = this.data.detectedLanguage || 'js'
let hasPackageJson = true
try {
await this.ctx.fs.access(path.join(this.ctx.currentProject, 'package.json'), this.ctx.fs.constants.R_OK)
} catch (e) {
debug('Could not read or find package.json: %O', e)
hasPackageJson = false
}
const packageJson: {
dependencies?: { [key: string]: string }
devDependencies?: { [key: string]: string }
} = hasPackageJson ? await this.ctx.fs.readJson(path.join(this.ctx.currentProject, 'package.json')) : {}
debug('packageJson %O', packageJson)
const dependencies = [
...Object.keys(packageJson.dependencies || {}),
...Object.keys(packageJson.devDependencies || {}),
]
this.detectFramework(dependencies)
debug('detectedFramework %s', this.data.detectedFramework)
this.detectBundler(dependencies)
debug('detectedBundler %s', this.data.detectedBundler)
this.data.chosenFramework = this.data.detectedFramework || null
this.data.chosenBundler = this.data.detectedBundler || null
}
}
private detectFramework (dependencies: string[]) {
// Detect full featured frameworks
if (dependencies.includes('next')) {
this.ctx.wizardData.detectedFramework = 'nextjs'
} else if (dependencies.includes('react-scripts')) {
this.ctx.wizardData.detectedFramework = 'cra'
} else if (dependencies.includes('nuxt')) {
this.ctx.wizardData.detectedFramework = 'nuxtjs'
} else if (dependencies.includes('@vue/cli-service')) {
this.ctx.wizardData.detectedFramework = 'vuecli'
} else if (dependencies.includes('react')) {
this.ctx.wizardData.detectedFramework = 'react'
} else if (dependencies.includes('vue')) {
this.ctx.wizardData.detectedFramework = 'vue'
}
}
private detectBundler (dependencies: string[]) {
const detectedFrameworkObject = FRONTEND_FRAMEWORKS.find((f) => f.type === this.ctx.wizardData.detectedFramework)
if (detectedFrameworkObject && detectedFrameworkObject.supportedBundlers.length === 1) {
this.ctx.wizardData.detectedBundler = detectedFrameworkObject.supportedBundlers[0] ?? null
if (!this.ctx.currentProject) {
return
}
if (dependencies.includes('webpack')) {
this.ctx.wizardData.detectedBundler = 'webpack'
}
this.ctx.update((coreData) => {
coreData.wizard.detectedFramework = null
coreData.wizard.detectedBundler = null
coreData.wizard.detectedLanguage = null
})
if (dependencies.includes('vite')) {
this.ctx.wizardData.detectedBundler = 'vite'
await this.detectLanguage()
debug('detectedLanguage %s', this.data.detectedLanguage)
this.data.chosenLanguage = this.data.detectedLanguage || 'js'
try {
const detected = detect(await fs.readJson(path.join(this.ctx.currentProject, 'package.json')))
debug('detected %o', detected)
if (detected) {
this.ctx.update((coreData) => {
coreData.wizard.detectedFramework = detected.framework?.type ?? null
coreData.wizard.chosenFramework = detected.framework?.type ?? null
if (!detected.framework?.supportedBundlers[0]) {
return
}
coreData.wizard.detectedBundler = detected.bundler || detected.framework.supportedBundlers[0].type
coreData.wizard.chosenBundler = detected.bundler || detected.framework.supportedBundlers[0].type
})
}
} catch {
// Could not detect anything - no problem, no need to do anything.
}
}
@@ -218,7 +188,6 @@ export class WizardActions {
this.scaffoldFixtures(),
this.scaffoldSupport('component', chosenLanguage.type),
this.getComponentIndexHtml({
chosenBundler,
chosenFramework,
chosenLanguage,
}),
@@ -252,11 +221,7 @@ export class WizardActions {
assert(chosenFramework && chosenLanguage && chosenBundler)
return this.wizardGetConfigCodeComponent({
chosenLanguage,
chosenFramework,
chosenBundler,
})
return chosenFramework.config[chosenLanguage.type](chosenBundler.type)
}
return this.wizardGetConfigCodeE2E(language)
@@ -315,28 +280,6 @@ export class WizardActions {
return codeBlocks.join('\n')
}
private wizardGetConfigCodeComponent (opts: WizardGetCodeComponent): string {
const codeBlocks: string[] = []
const { chosenBundler, chosenFramework, chosenLanguage } = opts
const requirePath = chosenFramework.defaultPackagePath ?? chosenBundler.package
codeBlocks.push(chosenLanguage.type === 'ts' ? `import { defineConfig } from 'cypress'` : `const { defineConfig } = require('cypress')`)
codeBlocks.push(chosenLanguage.type === 'ts' ? `import { devServer } from '${requirePath}'` : `const { devServer } = require('${requirePath}')`)
codeBlocks.push('')
codeBlocks.push(chosenLanguage.type === 'ts' ? `export default defineConfig({` : `module.exports = defineConfig({`)
codeBlocks.push(` // Component testing, ${chosenLanguage.name}, ${chosenFramework.name}, ${chosenBundler.name}`)
codeBlocks.push(` ${COMPONENT_SCAFFOLD_BODY({
lang: chosenLanguage.type,
configOptionsString: '{}',
}).replace(/\n/g, '\n ')}`)
codeBlocks.push(`})\n`)
return codeBlocks.join('\n')
}
private async getComponentIndexHtml (opts: WizardGetCodeComponent): Promise<NexusGenObjects['ScaffoldedFile']> {
const [storybookInfo] = await Promise.all([
this.ctx.storybook.loadStorybookInfo(),
@@ -442,21 +385,6 @@ const E2E_SCAFFOLD_BODY = dedent`
},
`
interface ComponentScaffoldOpts {
lang: CodeLanguageEnum
configOptionsString: string
specPattern?: string
}
const COMPONENT_SCAFFOLD_BODY = (opts: ComponentScaffoldOpts) => {
return dedent`
component: {
devServer,
devServerConfig: ${opts.configOptionsString}
},
`
}
const FIXTURE_DATA = {
'name': 'Using fixtures to represent data',
'email': 'hello@cypress.io',

View File

@@ -3,7 +3,7 @@ import type { DataContext } from '../DataContext'
import type { ParsedPath } from 'path'
import { camelCase, capitalize } from 'lodash'
import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen'
import type { CodeGenFramework } from '@packages/types'
import type { CodeGenFramework } from '@packages/scaffold-config'
import { CsfFile, readCsfOrMdx } from '@storybook/csf-tools'
interface CodeGenOptions {

View File

@@ -325,6 +325,8 @@ export class ProjectLifecycleManager {
setCurrentTestingType (testingType: TestingType | null) {
this.ctx.update((d) => {
d.currentTestingType = testingType
d.wizard.chosenBundler = null
d.wizard.chosenFramework = null
})
if (this._currentTestingType === testingType) {

View File

@@ -1,4 +1,5 @@
import { BUNDLERS, FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, PACKAGE_MANAGERS, BrowserStatus, AuthStateName } from '@packages/types'
import type { FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, BrowserStatus, PACKAGE_MANAGERS, AuthStateName } from '@packages/types'
import type { Bundler, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
import type { App, BrowserWindow } from 'electron'
import type { ChildProcess } from 'child_process'
@@ -61,14 +62,14 @@ export interface AppDataShape {
}
export interface WizardDataShape {
chosenBundler: NexusGenEnums['SupportedBundlers'] | null
allBundlers: typeof BUNDLERS
chosenFramework: NexusGenEnums['FrontendFrameworkEnum'] | null
chosenBundler: Bundler | null
chosenFramework: typeof FRONTEND_FRAMEWORKS[number]['type'] | null
chosenLanguage: NexusGenEnums['CodeLanguageEnum']
chosenManualInstall: boolean
detectedLanguage: NexusGenEnums['CodeLanguageEnum'] | null
detectedBundler: NexusGenEnums['SupportedBundlers'] | null
detectedFramework: NexusGenEnums['FrontendFrameworkEnum'] | null
detectedBundler: Bundler | null
detectedFramework: typeof FRONTEND_FRAMEWORKS[number]['type'] | null
__fakeInstalledPackagesForTesting: string[] | null
}
export interface MigrationDataShape{
@@ -168,9 +169,9 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
chosenFramework: null,
chosenLanguage: 'js',
chosenManualInstall: false,
allBundlers: BUNDLERS,
detectedBundler: null,
detectedFramework: null,
__fakeInstalledPackagesForTesting: null,
detectedLanguage: null,
},
migration: {

View File

@@ -1,5 +1,6 @@
import os from 'os'
import { FrontendFramework, FRONTEND_FRAMEWORKS, ResolvedFromConfig, RESOLVED_FROM, FoundSpec } from '@packages/types'
import type { ResolvedFromConfig, RESOLVED_FROM, FoundSpec } from '@packages/types'
import { FrontendFramework, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import { scanFSForAvailableDependency } from 'create-cypress-tests'
import minimatch from 'minimatch'
import { debounce, isEqual } from 'lodash'
@@ -261,7 +262,7 @@ export class ProjectDataSource {
private guessFramework (projectRoot: string) {
const guess = FRONTEND_FRAMEWORKS.find((framework) => {
const lookingForDeps = (framework.deps as readonly string[]).reduce(
const lookingForDeps = framework.detectors.map((x) => x.dependency).reduce(
(acc, dep) => ({ ...acc, [dep]: '*' }),
{},
)

View File

@@ -1,57 +1,84 @@
import Debug from 'debug'
import { BUNDLERS, CODE_LANGUAGES, FRONTEND_FRAMEWORKS, PACKAGES_DESCRIPTIONS } from '@packages/types'
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
import { CODE_LANGUAGES } from '@packages/types'
import {
BUNDLERS,
DEPENDENCIES,
FRONTEND_FRAMEWORKS,
} from '@packages/scaffold-config'
import type { DataContext } from '..'
import path from 'path'
import resolve from 'resolve-from'
import assert from 'assert'
const debug = Debug('cypress:data-context:wizard-data-source')
interface PackageToInstall {
name: typeof DEPENDENCIES[number]['name']
installer: typeof DEPENDENCIES[number]['installer']
description: typeof DEPENDENCIES[number]['description']
package: typeof DEPENDENCIES[number]['package']
}
export class WizardDataSource {
constructor (private ctx: DataContext) {}
async packagesToInstall (): Promise<Array<NexusGenObjects['WizardNpmPackage']> | null> {
async packagesToInstall (): Promise<PackageToInstall[] | null> {
if (!this.chosenFramework || !this.chosenBundler) {
return null
}
const packages = [
{
name: this.chosenFramework.name as string,
description: PACKAGES_DESCRIPTIONS[this.chosenFramework.package],
package: this.chosenFramework.package,
},
{
name: this.chosenBundler.name as string,
description: PACKAGES_DESCRIPTIONS[this.chosenBundler.package],
package: this.chosenBundler.package as string,
},
]
const packages: PackageToInstall[] = [...this.chosenFramework.packages]
// find the matching dev server
// vite -> @cypress/vite-dev-server
// webpack -> @cypress/webpack-dev-server
const bundler = BUNDLERS.find((x) => x.type === this.chosenBundler?.type)
assert(bundler)
packages.push(...bundler.dependencies)
const storybookInfo = await this.ctx.storybook.loadStorybookInfo()
const { storybookDep } = this.chosenFramework
if (storybookInfo && storybookDep) {
packages.push({
name: storybookDep,
description: PACKAGES_DESCRIPTIONS[storybookDep],
package: storybookDep,
})
packages.push(storybookDep)
}
return packages
}
async resolvePackagesToInstall (): Promise<string[]> {
async installDependenciesCommand () {
const commands = {
'npm': 'npm install -D',
'pnpm': 'pnpm install -D',
'yarn': 'yarn add -D',
}
let depsToInstall = (await this.ctx.wizard.packagesToInstall() ?? [])
if (!depsToInstall?.length) {
return null
}
const deps = depsToInstall.map((pack) => `${pack.installer}`).join(' ')
return `${commands[this.ctx.coreData.packageManager ?? 'npm']} ${deps}`
}
async installedPackages (): Promise<string[]> {
if (this.ctx.coreData.wizard.__fakeInstalledPackagesForTesting) {
return this.ctx.coreData.wizard.__fakeInstalledPackagesForTesting
}
const packagesInitial = await this.packagesToInstall() || []
if (!this.ctx.currentProject) {
throw Error('currentProject is not defined')
}
debug('packages to install: %O', packagesInitial)
debug('packages to install: %O in %s', packagesInitial, this.ctx.currentProject)
const installedPackages: (string|null)[] = packagesInitial.map((p) => {
const installedPackages: Array<string | null> = packagesInitial.map((p) => {
if (this.ctx.currentProject) {
debug('package checked: %s', p.package)
@@ -62,6 +89,8 @@ export class WizardDataSource {
// `package.json`
const packageJsonPath = path.join(p.package, 'package.json')
debug('package.json path: %s', packageJsonPath)
try {
resolve(this.ctx.currentProject, packageJsonPath)

View File

@@ -1,21 +1,17 @@
import { parse } from '@babel/parser'
import { graphqlSchema } from '@packages/graphql/src/schema'
import { FRONTEND_FRAMEWORKS } from '@packages/types'
import { FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import { expect } from 'chai'
import dedent from 'dedent'
import fs from 'fs-extra'
import path from 'path'
import sinon from 'sinon'
import { DataContext } from '../../../src'
import { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../../src/actions'
import {
Action, codeGenerator, CodeGenResult, CodeGenResults,
} from '../../../src/codegen/code-generator'
import { SpecOptions } from '../../../src/codegen/spec-options'
import templates from '../../../src/codegen/templates'
import { InjectedConfigApi } from '../../../src/data'
import { ErrorApiShape } from '../../../src/DataContext'
import { BrowserApiShape } from '../../../src/sources'
import { createTestDataContext } from '../helper'
const tmpPath = path.join(__dirname, 'tmp/test-code-gen')
@@ -29,21 +25,7 @@ describe('code-generator', () => {
let ctx: DataContext
beforeEach(async () => {
ctx = new DataContext({
schema: graphqlSchema,
mode: 'run',
modeOptions: {},
appApi: {} as AppApiShape,
localSettingsApi: {} as LocalSettingsApiShape,
authApi: {} as AuthApiShape,
errorApi: {} as ErrorApiShape,
configApi: {
getServerPluginHandlers: () => [],
} as InjectedConfigApi,
projectApi: {} as ProjectApiShape,
electronApi: {} as ElectronApiShape,
browserApi: {} as BrowserApiShape,
})
ctx = createTestDataContext()
ctx.update((s) => {
s.currentProject = tmpPath

View File

@@ -2,6 +2,11 @@
import 'mocha'
import { e2eProjectDirs } from '@packages/frontend-shared/cypress/e2e/support/e2eProjectDirs'
import Fixtures from '@tooling/system-tests/lib/fixtures'
import { DataContext, DataContextConfig } from '../../src'
import { graphqlSchema } from '@packages/graphql/src/schema'
import type { BrowserApiShape } from '../../src/sources/BrowserDataSource'
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../src/actions'
import { InjectedConfigApi } from '../../src/data'
export async function scaffoldMigrationProject (project: typeof e2eProjectDirs[number]) {
Fixtures.removeProject(project)
@@ -10,3 +15,20 @@ export async function scaffoldMigrationProject (project: typeof e2eProjectDirs[n
return Fixtures.projectPath(project)
}
export function createTestDataContext (mode: DataContextConfig['mode'] = 'run') {
return new DataContext({
schema: graphqlSchema,
mode,
modeOptions: {},
appApi: {} as AppApiShape,
localSettingsApi: {} as LocalSettingsApiShape,
authApi: {} as AuthApiShape,
configApi: {
getServerPluginHandlers: () => [],
} as InjectedConfigApi,
projectApi: {} as ProjectApiShape,
electronApi: {} as ElectronApiShape,
browserApi: {} as BrowserApiShape,
})
}

View File

@@ -1,12 +1,9 @@
import { expect } from 'chai'
import { graphqlSchema } from '@packages/graphql/src/schema'
import os from 'os'
import sinon from 'sinon'
import { DataContext } from '../../../src'
import { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape } from '../../../src/actions'
import { InjectedConfigApi } from '../../../src/data'
import { ErrorApiShape } from '../../../src/DataContext'
import { BrowserApiShape, VersionsDataSource } from '../../../src/sources'
import { VersionsDataSource } from '../../../src/sources'
import { createTestDataContext } from '../helper'
const pkg = require('@packages/root')
const nmi = require('node-machine-id')
@@ -21,21 +18,7 @@ describe('VersionsDataSource', () => {
let currentCypressVersion: string = pkg.version
before(() => {
ctx = new DataContext({
schema: graphqlSchema,
mode: 'open',
modeOptions: {},
appApi: {} as AppApiShape,
localSettingsApi: {} as LocalSettingsApiShape,
authApi: {} as AuthApiShape,
errorApi: {} as ErrorApiShape,
configApi: {
getServerPluginHandlers: () => [],
} as InjectedConfigApi,
projectApi: {} as ProjectApiShape,
electronApi: {} as ElectronApiShape,
browserApi: {} as BrowserApiShape,
})
ctx = createTestDataContext('open')
ctx.coreData.currentTestingType = 'e2e'

View File

@@ -0,0 +1,119 @@
import { expect } from 'chai'
import path from 'path'
import { e2eProjectDirs } from '@packages/frontend-shared/cypress/e2e/support/e2eProjectDirs'
import { createTestDataContext } from '../helper'
function getCurrentProject (project: typeof e2eProjectDirs[number]) {
return path.join(__dirname, '..', '..', '..', '..', '..', 'system-tests', 'projects', project)
}
describe('packagesToInstall', () => {
it('create-react-app-unconfigured', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('create-react-app-unconfigured')
ctx.coreData.wizard.chosenFramework = 'crav5'
ctx.coreData.wizard.chosenBundler = 'webpack5'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/react@^5.0.0 webpack@^5.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^5.0.0`)
})
it('vueclivue2-unconfigured', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('vueclivue2-unconfigured')
ctx.coreData.wizard.chosenFramework = 'vueclivue2'
ctx.coreData.wizard.chosenBundler = 'webpack4'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/vue@^2.0.0 webpack@^4.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^4.0.0`)
})
it('vueclivue3-unconfigured', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('vueclivue3-unconfigured')
ctx.coreData.wizard.chosenFramework = 'vueclivue3'
ctx.coreData.wizard.chosenBundler = 'webpack4'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/vue@^3.0.0 webpack@^4.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^4.0.0`)
})
it('regular react project with vite', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('react-vite-ts-unconfigured')
ctx.coreData.wizard.chosenFramework = 'react'
ctx.coreData.wizard.chosenBundler = 'vite'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/react@^5.0.0 @cypress/vite-dev-server@latest`)
})
it('regular vue project with vite', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('vue3-vite-ts-unconfigured')
ctx.coreData.wizard.chosenFramework = 'vue3'
ctx.coreData.wizard.chosenBundler = 'vite'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/vue@^3.0.0 @cypress/vite-dev-server@latest`)
})
it('nextjs-unconfigured', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('nextjs-unconfigured')
ctx.coreData.wizard.chosenFramework = 'nextjs'
ctx.coreData.wizard.chosenBundler = 'webpack4'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D @cypress/react@^5.0.0 webpack@^4.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^4.0.0`)
})
it('nuxtjs-vue2-unconfigured', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('nuxtjs-vue2-unconfigured')
ctx.coreData.wizard.chosenFramework = 'nuxtjs'
ctx.coreData.wizard.chosenBundler = 'webpack4'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq('npm install -D @cypress/vue@^2.0.0 webpack@^4.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^4.0.0')
})
it('pristine-with-e2e-testing-and-storybook', async () => {
const ctx = createTestDataContext()
ctx.coreData.currentProject = getCurrentProject('pristine-with-e2e-testing-and-storybook')
ctx.coreData.wizard.chosenFramework = 'react'
ctx.coreData.wizard.chosenBundler = 'webpack4'
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq('npm install -D @cypress/react@^5.0.0 @cypress/webpack-dev-server@latest webpack-dev-server@^4.0.0 html-webpack-plugin@^4.0.0 @storybook/testing-react@latest')
})
it('framework and bundler are undefined', async () => {
const ctx = createTestDataContext()
// this should never happen!
ctx.coreData.currentProject = getCurrentProject('pristine-with-e2e-testing-and-storybook')
ctx.coreData.wizard.chosenFramework = undefined
ctx.coreData.wizard.chosenBundler = undefined
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.be.null
})
})

View File

@@ -13,6 +13,8 @@ export const e2eProjectDirs = [
'config-with-short-timeout',
'config-with-ts',
'cookies',
'create-react-app-configured',
'create-react-app-unconfigured',
'cypress-in-cypress',
'default-layout',
'devServer-dynamic-import',
@@ -49,6 +51,8 @@ export const e2eProjectDirs = [
'multiple-config-files-with-json',
'multiple-support-files',
'multiple-task-registrations',
'nextjs-configured',
'nextjs-unconfigured',
'no-scaffolding',
'no-server',
'no-specs',
@@ -56,6 +60,8 @@ export const e2eProjectDirs = [
'no-specs-no-storybook',
'non-existent-spec',
'non-proxied',
'nuxtjs-vue2-configured',
'nuxtjs-vue2-unconfigured',
'odd-directory-name',
'plugin-after-screenshot',
'plugin-after-spec-deletes-video',
@@ -88,7 +94,10 @@ export const e2eProjectDirs = [
'pristine-with-e2e-testing',
'pristine-with-e2e-testing-and-storybook',
'pristine-yarn',
'react-app-webpack-5-unconfigured',
'react-code-gen',
'react-vite-ts-configured',
'react-vite-ts-unconfigured',
'read-only-project-root',
'record',
'remote-debugging-disconnect',
@@ -120,6 +129,12 @@ export const e2eProjectDirs = [
'unify-plugin-errors',
'various-file-types',
'vite-ct',
'vue3-vite-ts-configured',
'vue3-vite-ts-unconfigured',
'vueclivue2-configured',
'vueclivue2-unconfigured',
'vueclivue3-configured',
'vueclivue3-unconfigured',
'webpack-preprocessor',
'webpack-preprocessor-awesome-typescript-loader',
'webpack-preprocessor-ts-loader',

View File

@@ -1,5 +1,6 @@
import type { CodegenTypeMap, Wizard } from '../generated/test-graphql-types.gen'
import { BUNDLERS, CODE_LANGUAGES, FRONTEND_FRAMEWORKS, PACKAGES_DESCRIPTIONS } from '@packages/types/src/constants'
import { CODE_LANGUAGES } from '@packages/types/src/constants'
import { BUNDLERS, CYPRESS_REACT_LATEST, CYPRESS_WEBPACK, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import type { MaybeResolver } from './clientTestUtils'
import { testNodeId } from './clientTestUtils'
@@ -14,18 +15,15 @@ export const allBundlers = BUNDLERS.map((bundler, idx) => {
export const stubWizard: MaybeResolver<Wizard> = {
__typename: 'Wizard',
installDependenciesCommand: 'npm install -D @cypress/react @cypress/webpack-dev-server',
packagesToInstall: [
{
...testNodeId('WizardNpmPackage'),
description: PACKAGES_DESCRIPTIONS['@cypress/react'],
name: '@cypress/react',
package: '@cypress/react',
...CYPRESS_REACT_LATEST,
},
{
...testNodeId('WizardNpmPackage'),
description: PACKAGES_DESCRIPTIONS['@cypress/webpack-dev-server'],
name: '@cypress/webpack-dev-server',
package: '@cypress/webpack-dev-server',
...CYPRESS_WEBPACK,
},
],
allBundlers,

View File

@@ -511,12 +511,15 @@ enum FrontendFrameworkCategoryEnum {
}
enum FrontendFrameworkEnum {
cra
crav4
crav5
nextjs
nuxtjs
react
vue
vuecli
vue2
vue3
vueclivue2
vueclivue3
}
"""Error from generated spec"""
@@ -1052,6 +1055,13 @@ type Storybook implements Node {
"""The bundlers that we can use with Cypress"""
enum SupportedBundlers {
vite
webpack4
webpack5
}
"""The packages for bundlers that we can use with Cypress"""
enum SupportedPackage {
vite
webpack
}
@@ -1113,6 +1123,9 @@ type Wizard {
"""All of the component testing frameworks to choose from"""
frameworks: [WizardFrontendFramework!]!
"""Command to install required command"""
installDependenciesCommand: String
"""The list of packages to install that are currently installed"""
installedPackages: [String!]
language: WizardCodeLanguage
@@ -1137,8 +1150,8 @@ type WizardBundler implements Node {
"""Display name of the bundler"""
name: String!
"""Package to install associated with the bundler"""
package: String!
"""Name of package on npm"""
package: SupportedPackage!
"""The name of the framework"""
type: SupportedBundlers!

View File

@@ -1,4 +1,5 @@
import { BUNDLERS, CODE_LANGUAGES, FRONTEND_FRAMEWORKS } from '@packages/types'
import { CODE_LANGUAGES } from '@packages/types'
import { BUNDLERS, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import { enumType } from 'nexus'
export const SupportedBundlerEnum = enumType({
@@ -7,6 +8,12 @@ export const SupportedBundlerEnum = enumType({
members: BUNDLERS.map((t) => t.type),
})
export const SupportedPackageEnum = enumType({
name: 'SupportedPackage',
description: 'The packages for bundlers that we can use with Cypress',
members: BUNDLERS.map((t) => t.package),
})
export const WizardConfigFileStatusEnum = enumType({
name: 'WizardConfigFileStatusEnum',
members: ['changes', 'valid', 'skipped', 'error'],

View File

@@ -2,7 +2,8 @@ import { WizardBundler } from './gql-WizardBundler'
import { WizardFrontendFramework } from './gql-WizardFrontendFramework'
import { WizardNpmPackage } from './gql-WizardNpmPackage'
import { objectType } from 'nexus'
import { BUNDLERS, CODE_LANGUAGES, FRONTEND_FRAMEWORKS } from '@packages/types'
import { CODE_LANGUAGES } from '@packages/types'
import { BUNDLERS, FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
import { WizardCodeLanguage } from './gql-WizardCodeLanguage'
export const Wizard = objectType({
@@ -50,7 +51,12 @@ export const Wizard = objectType({
t.list.nonNull.string('installedPackages', {
description: 'The list of packages to install that are currently installed',
resolve: (source, args, ctx) => ctx.wizard.resolvePackagesToInstall(),
resolve: (source, args, ctx) => ctx.wizard.installedPackages(),
})
t.string('installDependenciesCommand', {
description: 'Command to install required command',
resolve: (source, args, ctx) => ctx.wizard.installDependenciesCommand(),
})
},
sourceType: {

View File

@@ -1,10 +1,10 @@
import { objectType } from 'nexus'
import { SupportedBundlerEnum } from '../enumTypes'
import { SupportedBundlerEnum, SupportedPackageEnum } from '../enumTypes'
export const WizardBundler = objectType({
name: 'WizardBundler',
description: 'Wizard bundler',
node: 'name',
node: 'type',
definition (t) {
t.boolean('isSelected', {
description: 'Whether this is the selected framework bundler',
@@ -25,8 +25,9 @@ export const WizardBundler = objectType({
description: 'Display name of the bundler',
})
t.nonNull.string('package', {
description: 'Package to install associated with the bundler',
t.nonNull.field('package', {
type: SupportedPackageEnum,
description: 'Name of package on npm',
})
},
})

View File

@@ -35,12 +35,13 @@ export const WizardFrontendFramework = objectType({
type: WizardBundler,
description: 'All of the supported bundlers for this framework',
resolve: (source, args, ctx) => {
return ctx.wizardData.allBundlers.filter((b) => source.supportedBundlers.some((x) => x === b.type))
return [...source.supportedBundlers]
},
})
},
sourceType: {
module: '@packages/types',
module: '@packages/scaffold-config',
export: 'FrontendFramework',
},
})

View File

@@ -1,4 +1,15 @@
import { FRONTEND_FRAMEWORKS, BUNDLERS, CODE_LANGUAGES, PACKAGES_DESCRIPTIONS } from '@packages/types/src/constants'
import { BUNDLERS, FRONTEND_FRAMEWORKS, AllPackagePackages } from '@packages/scaffold-config/src'
import { CODE_LANGUAGES } from '@packages/types/src'
function fakeInstalledDeps () {
cy.withCtx(async (ctx) => {
const deps = (await ctx.wizard.packagesToInstall() ?? []).map((x) => x.package)
ctx.update((coreData) => {
coreData.wizard.__fakeInstalledPackagesForTesting = deps
})
})
}
describe('Launchpad: Setup Project', () => {
function scaffoldAndOpenProject (name: Parameters<typeof cy.scaffoldProject>[0], args?: Parameters<typeof cy.openProject>[1]) {
@@ -349,7 +360,7 @@ describe('Launchpad: Setup Project', () => {
cy.findByText('Confirm the front-end framework and bundler used in your project.')
cy.findByRole('button', { name: 'Front-end Framework Pick a framework' }).click()
cy.findByRole('option', { name: 'Create React App' }).click()
cy.findByRole('option', { name: 'Create React App (v4)' }).click()
cy.get('[data-testid="select-bundler"').should('not.exist')
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
@@ -363,42 +374,23 @@ describe('Launchpad: Setup Project', () => {
cy.findByRole('button', { name: 'Next Step' }).should('have.disabled')
cy.findByRole('button', { name: 'Bundler(Dev Server) Pick a bundler' }).click()
cy.findByRole('option', { name: 'Webpack' }).click()
cy.findByRole('option', { name: 'Webpack (v4)' }).click()
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
cy.findByRole('button', { name: 'Front-end Framework React.js' }).click()
cy.findByRole('option', { name: 'Create React App' }).click()
cy.findByRole('option', { name: 'Create React App (v4)' }).click()
cy.findByRole('button', { name: 'Bundler(Dev Server) Webpack' }).should('not.exist')
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
cy.findByRole('button', { name: 'TypeScript' }).click()
let calls = 0
// simulate progressive installation of modules
cy.intercept('query-Wizard_InstalledPackages', (req) => {
req.reply({ data: {
wizard: {
__typename: 'Wizard',
installedPackages: ++calls <= 2 ? [] :
calls <= 3 ? ['@cypress/react'] :
['@cypress/react', '@cypress/webpack-dev-server'],
},
} })
}).as('InstalledPackages')
cy.findByRole('button', { name: 'Next Step' }).click()
cy.findByRole('button', { name: 'Waiting for you to install the dependencies...' })
cy.wait('@InstalledPackages')
fakeInstalledDeps()
cy.contains('li', '@cypress/react').findByLabelText('installed').should('be.visible')
cy.wait('@InstalledPackages')
cy.contains('li', '@cypress/webpack-dev-server').findByLabelText('installed').should('be.visible').then(() => {
expect(calls).to.eq(4)
})
cy.findByRole('button', { name: 'Continue' }).click()
cy.get('[data-cy=changes]').within(() => {
@@ -552,196 +544,149 @@ describe('Launchpad: Setup Project', () => {
})
})
describe('project that has not been configured for component testing', () => {
beforeEach(() => {
// simulate installation of modules
cy.intercept('query-Wizard_InstalledPackages', (req) => {
req.reply({ data: {
wizard: {
__typename: 'Wizard',
installedPackages: [
'@cypress/react',
'@cypress/vue',
'@cypress/webpack-dev-server',
'@cypress/vite-dev-server',
'@storybook/testing-react',
'@storybook/testing-vue',
'@storybook/testing-vue3',
],
},
} })
}).as('InstalledPackages')
})
beforeEach(() => {
fakeInstalledDeps()
})
it('shows the first setup page for configuration when selecting component tests', () => {
scaffoldAndOpenProject('pristine-with-e2e-testing')
cy.visitLaunchpad()
const hasStorybookPermutations = [false, true]
verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false })
FRONTEND_FRAMEWORKS.forEach((framework) => {
hasStorybookPermutations.forEach((hasStorybookDep) => {
framework.supportedBundlers.forEach((testBundler) => {
const bundler = BUNDLERS.find((b) => b.type === testBundler.type)
cy.get('[data-cy-testingtype="component"]').click()
if (!bundler) {
throw new Error(`${framework.name} claims to support the bundler, ${testBundler}, however it is not a valid Cypress bundler.`)
}
cy.get('h1').should('contain', 'Project Setup')
cy.contains('Confirm the front-end framework and bundler used in your project.')
CODE_LANGUAGES.forEach((lang) => {
let testTitle = `can setup ${framework.name} + ${lang.name}`
cy.findByRole('button', {
name: 'Front-end Framework Pick a framework',
expanded: false,
})
.should('have.attr', 'aria-haspopup', 'true')
cy.findByRole('button', { name: 'Next Step' }).should('have.disabled')
cy.findByRole('button', { name: 'Back' }).click()
verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false })
})
const hasStorybookPermutations = [false, true]
FRONTEND_FRAMEWORKS.forEach((framework) => {
hasStorybookPermutations.forEach((hasStorybookDep) => {
framework.supportedBundlers.forEach((testBundler) => {
const bundler = BUNDLERS.find((b) => b.type === testBundler)
if (!bundler) {
throw new Error(`${framework.name} claims to support the bundler, ${testBundler}, however it is not a valid Cypress bundler.`)
if (framework.supportedBundlers.length > 1) {
testTitle = `can setup ${framework.name} + ${bundler.name} + ${lang.name}`
}
CODE_LANGUAGES.forEach((lang) => {
let testTitle = `can setup ${framework.name} + ${lang.name}`
if (hasStorybookDep) {
testTitle += ` for project using Storybook`
}
it(testTitle, () => {
scaffoldAndOpenProject(hasStorybookDep ? 'pristine-with-e2e-testing-and-storybook' : 'pristine-with-e2e-testing')
cy.withCtx((ctx) => {
ctx.actions.file.writeFileInProject('yarn.lock', '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.')
})
cy.visitLaunchpad()
verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false })
cy.get('[data-cy-testingtype="component"]').click()
cy.log('Choose project setup')
cy.get('h1').should('contain', 'Project Setup')
cy.contains('Confirm the front-end framework and bundler used in your project.')
cy.findByRole('button', { name: 'Next Step' })
.should('have.disabled')
.as('nextStepButton')
cy.findByRole('button', {
name: 'Front-end Framework Pick a framework',
expanded: false,
})
.click()
cy.findByRole('option', { name: framework.name }).click()
cy.findByRole('button', { name: `Front-end Framework ${framework.name}` }) // ensure selected option updates
if (framework.supportedBundlers.length > 1) {
testTitle = `can setup ${framework.name} + ${bundler.name} + ${lang.name}`
}
if (hasStorybookDep) {
testTitle += ` for project using Storybook`
}
it(testTitle, () => {
scaffoldAndOpenProject(hasStorybookDep ? 'pristine-with-e2e-testing-and-storybook' : 'pristine-with-e2e-testing')
cy.withCtx((ctx) => {
ctx.actions.file.writeFileInProject('yarn.lock', '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.')
})
cy.visitLaunchpad()
verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false })
cy.get('[data-cy-testingtype="component"]').click()
cy.log('Choose project setup')
cy.get('h1').should('contain', 'Project Setup')
cy.contains('Confirm the front-end framework and bundler used in your project.')
cy.findByRole('button', { name: 'Next Step' })
.should('have.disabled')
.as('nextStepButton')
cy.findByRole('button', {
name: 'Front-end Framework Pick a framework',
name: 'Bundler(Dev Server) Pick a bundler',
expanded: false,
})
.should('have.attr', 'aria-haspopup', 'true')
.click()
.should('have.attr', 'aria-expanded', 'true')
cy.findByRole('option', { name: framework.name }).click()
cy.findByRole('button', { name: `Front-end Framework ${framework.name}` }) // ensure selected option updates
if (framework.supportedBundlers.length > 1) {
cy.findByRole('button', {
name: 'Bundler(Dev Server) Pick a bundler',
expanded: false,
})
.should('have.attr', 'aria-haspopup', 'true')
.click()
.should('have.attr', 'aria-expanded', 'true')
framework.supportedBundlers.forEach((supportedBundler) => {
cy.findByRole('option', { name: Cypress._.startCase(supportedBundler) })
.find('svg')
.should('have.attr', 'data-cy', `${Cypress._.lowerCase(supportedBundler)}-logo`)
})
cy.findByRole('option', { name: bundler.name })
framework.supportedBundlers.forEach((supportedBundler) => {
cy.findByRole('option', { name: supportedBundler.name })
.find('svg')
.should('have.attr', 'data-cy', `${Cypress._.lowerCase(bundler.name)}-logo`)
.click()
.should('have.attr', 'data-cy', `${supportedBundler.package}-logo`)
})
cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` }) // ensure selected option updates
}
cy.findByRole('button', { name: lang.name }).click()
cy.log('Go to next step')
cy.get('@nextStepButton').should('not.have.disabled').click()
cy.contains('h1', 'Install Dev Dependencies')
cy.contains('p', 'Paste the command below into your terminal to install the required packages.')
cy.log('Return to previous step')
cy.findByRole('button', { name: 'Back' })
cy.findByRole('option', { name: bundler.name })
.find('svg')
.should('have.attr', 'data-cy', `${Cypress._.lowerCase(bundler.package)}-logo`)
.click()
cy.findByRole('button', { name: `Front-end Framework ${framework.name}` })
if (framework.supportedBundlers.length > 1) {
cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` })
}
cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` }) // ensure selected option updates
}
cy.findByRole('button', { name: lang.name })
cy.findByRole('button', { name: 'Next Step' }).click()
cy.findByRole('button', { name: lang.name }).click()
cy.log('Go to next step and verify Install Dev Dependencies page')
cy.contains('h1', 'Install Dev Dependencies')
cy.log('Go to next step')
cy.get('@nextStepButton').should('not.have.disabled').click()
let installCommand = `npm install -D ${framework.package} ${bundler.package}`
cy.contains('h1', 'Install Dev Dependencies')
cy.contains('p', 'Paste the command below into your terminal to install the required packages.')
if (hasStorybookDep) {
installCommand += ` ${framework.storybookDep}`
}
cy.log('Return to previous step')
cy.findByRole('button', { name: 'Back' })
.click()
cy.contains('code', installCommand)
cy.findByRole('button', { name: `Front-end Framework ${framework.name}` })
if (framework.supportedBundlers.length > 1) {
cy.findByRole('button', { name: `Bundler(Dev Server) ${bundler.name}` })
}
const validatePackage = (packageName) => {
cy.validateExternalLink({
name: packageName,
href: `https://www.npmjs.com/package/${packageName}`,
})
cy.findByRole('button', { name: lang.name })
cy.findByRole('button', { name: 'Next Step' }).click()
cy.contains(PACKAGES_DESCRIPTIONS[framework.package].split('<span')[0])
}
cy.log('Go to next step and verify Install Dev Dependencies page')
cy.contains('h1', 'Install Dev Dependencies')
validatePackage(framework.package)
validatePackage(bundler.package)
if (hasStorybookDep) {
validatePackage(framework.storybookDep)
}
cy.findByRole('button', { name: 'Continue' }).click()
// Even if user chooses typescript in the previous
// step, they already have a config file in js.
// We cannot rename this file for them.
cy.contains('[data-cy=changes]', `cypress.config.js`)
cy.get('[data-cy=valid]').within(() => {
cy.containsPath('cypress/component/index.html')
cy.containsPath(`cypress/support/component.${lang.type}`)
cy.containsPath('cypress/fixtures/example.json')
const validatePackage = (packageName: AllPackagePackages) => {
cy.validateExternalLink({
name: packageName,
href: `https://www.npmjs.com/package/${packageName}`,
})
}
[...framework.packages].forEach((pkg) => {
cy.contains(pkg.description.split('<span')[0])
validatePackage(pkg.package)
})
fakeInstalledDeps()
if (hasStorybookDep && framework.storybookDep) {
validatePackage(framework.storybookDep.package)
}
cy.findByRole('button', { name: 'Continue' }).click()
// Even if user chooses typescript in the previous
// step, they already have a config file in js.
// We cannot rename this file for them.
cy.contains('[data-cy=changes]', `cypress.config.js`)
cy.get('[data-cy=valid]').within(() => {
cy.containsPath('cypress/component/index.html')
cy.containsPath(`cypress/support/component.${lang.type}`)
cy.containsPath('cypress/fixtures/example.json')
})
})
})
})
})
})
it('opens to the "choose framework" page when opened via cli with --component flag', () => {
scaffoldAndOpenProject('pristine-with-e2e-testing', ['--component'])
cy.visitLaunchpad()
it('opens to the "choose framework" page when opened via cli with --component flag', () => {
scaffoldAndOpenProject('pristine-with-e2e-testing', ['--component'])
cy.visitLaunchpad()
cy.get('h1').should('contain', 'Project Setup')
cy.contains('Confirm the front-end framework and bundler used in your project.')
})
cy.get('h1').should('contain', 'Project Setup')
cy.contains('Confirm the front-end framework and bundler used in your project.')
})
describe('project not been configured for cypress', () => {
@@ -758,7 +703,7 @@ describe('Launchpad: Setup Project', () => {
cy.findByText('Confirm the front-end framework and bundler used in your project.')
cy.findByRole('button', { name: 'Front-end Framework Pick a framework' }).click()
cy.findByRole('option', { name: 'Create React App' }).click()
cy.findByRole('option', { name: 'Create React App (v4)' }).click()
cy.get('[data-testid="select-bundler"').should('not.exist')
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
@@ -772,17 +717,20 @@ describe('Launchpad: Setup Project', () => {
cy.findByRole('button', { name: 'Next Step' }).should('have.disabled')
cy.findByRole('button', { name: 'Bundler(Dev Server) Pick a bundler' }).click()
cy.findByRole('option', { name: 'Webpack' }).click()
cy.findByRole('option', { name: 'Webpack (v4)' }).click()
cy.findByRole('button', { name: 'TypeScript' }).click()
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
cy.findByRole('button', { name: 'Front-end Framework React.js' }).click()
cy.findByRole('option', { name: 'Create React App' }).click()
cy.findByRole('option', { name: 'Create React App (v4)' }).click()
cy.findByRole('button', { name: 'Bundler(Dev Server) Webpack' }).should('not.exist')
cy.findByRole('button', { name: 'Next Step' }).should('not.have.disabled')
cy.findByRole('button', { name: 'Next Step' }).click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
cy.get('[data-cy=valid]').within(() => {
@@ -809,8 +757,11 @@ describe('Launchpad: Setup Project', () => {
cy.findByText('Confirm the front-end framework and bundler used in your project.')
cy.findByRole('button', { name: 'Front-end Framework Pick a framework' }).click()
cy.findByRole('option', { name: 'Create React App' }).click()
cy.findByRole('option', { name: 'Create React App (v4)' }).click()
cy.findByRole('button', { name: 'TypeScript' }).click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Next Step' }).click()
cy.findByRole('button', { name: 'Continue' }).click()
@@ -835,7 +786,7 @@ describe('Launchpad: Setup Project', () => {
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').click()
cy.findByText('Create React App').click()
cy.findByText('Create React App (v4)').click()
cy.findByText('Next Step').click()
cy.get('code').should('contain.text', 'yarn add -D ')
})
@@ -847,7 +798,7 @@ describe('Launchpad: Setup Project', () => {
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').click()
cy.findByText('Create React App').click()
cy.findByText('Create React App (v4)').click()
cy.findByText('Next Step').click()
cy.get('code').should('contain.text', 'pnpm install -D ')
})
@@ -859,165 +810,9 @@ describe('Launchpad: Setup Project', () => {
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').click()
cy.findByText('Create React App').click()
cy.findByText('Create React App (v4)').click()
cy.findByText('Next Step').click()
cy.get('code').should('contain.text', 'npm install -D ')
})
})
describe('detect framework, bundler and language', () => {
beforeEach(() => {
scaffoldAndOpenProject('pristine')
})
context('meta frameworks', () => {
it('detects CRA framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"react": "^1.0.0",
"react-dom": "^1.0.1",
"react-scripts": "1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('Create React App').should('be.visible')
})
it('detects Next framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"react": "^1.0.0",
"react-dom": "^1.0.1",
"next": "1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('Next.js').should('be.visible')
})
it('detects vue-cli framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"vue": "^1.0.0",
"@vue/cli-service": "^1.0.1"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('Vue CLI').should('be.visible')
})
it('detects nuxtjs framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"vue": "^1.0.0",
"nuxt": "^1.0.1"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('Nuxt.js').should('be.visible')
})
})
context('pure frameworks', () => {
it('detects react framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"react": "^1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('React.js').should('be.visible')
})
it('detects vue framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"vue": "^1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-framework"]').findByText('Vue.js').should('be.visible')
})
})
describe('bundlers', () => {
it('detects webpack framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"react": "^1.0.0",
"webpack": "^1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-bundler"]').findByText('Webpack').should('be.visible')
})
it('detects vite framework', () => {
cy.withCtx(async (ctx) => {
await ctx.actions.file.writeFileInProject('package.json', `
{
"dependencies": {
"react": "^1.0.0",
"vite": "^1.0.0"
}
}
`)
})
cy.visitLaunchpad()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-testid="select-bundler"]').findByText('Vite').should('be.visible')
})
})
})
})

View File

@@ -0,0 +1,121 @@
import type { e2eProjectDirs } from '@packages/frontend-shared/cypress/e2e/support/e2eProjectDirs'
function startSetupFor (project: typeof e2eProjectDirs[number]) {
cy.scaffoldProject(project)
cy.openProject(project)
cy.visitLaunchpad()
cy.contains('Component Testing').click()
cy.get(`[data-testid="select-framework"]`)
}
function verifyConfigFile (configFile: `cypress.config.${'js' | 'ts'}`) {
cy.withCtx(async (ctx, o) => {
const configStats = await ctx.actions.file.checkIfFileExists(o.configFile)
expect(configStats).to.not.be.null.and.not.be.undefined
await ctx.actions.migration.assertSuccessfulConfigScaffold(o.configFile)
}, { configFile })
}
function fakeInstalledDeps () {
cy.withCtx(async (ctx) => {
const deps = (await ctx.wizard.packagesToInstall() ?? []).map((x) => x.package)
ctx.update((coreData) => {
coreData.wizard.__fakeInstalledPackagesForTesting = deps
})
})
}
describe('scaffolding component testing', () => {
context('vueclivue2', () => {
it('scaffolds component testing for Vue CLI w/ Vue 2 project', () => {
startSetupFor('vueclivue2-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('Vue CLI (Vue 2)(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.js`)
})
})
context('vueclivue3', () => {
it('scaffolds component testing for Vue CLI w/ Vue 3 project', () => {
startSetupFor('vueclivue3-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('Vue CLI (Vue 3)(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.js`)
})
})
context('create-react-app', () => {
it('scaffolds component testing for Vue CLI w/ Vue 2 project', () => {
startSetupFor('create-react-app-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('Create React App (v5)(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.js`)
})
})
context('react-vite-ts-unconfigured', () => {
it('scaffolds component testing for React and Vite', () => {
startSetupFor('react-vite-ts-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('React.js(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.ts`)
})
})
context('vue3-vite-ts-unconfigured', () => {
it('scaffolds component testing for Vue 3 and Vite', () => {
startSetupFor('vue3-vite-ts-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('Vue.js (v3)(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.ts`)
})
})
context('nuxtjs-vue2-unconfigured', () => {
it('scaffolds component testing for Nuxt 2', () => {
startSetupFor('nuxtjs-vue2-unconfigured')
// should detect correctly
cy.get('button').should('be.visible').contains('Nuxt.js (v2)(detected)')
cy.get('button').contains('Next Step').click()
fakeInstalledDeps()
cy.findByRole('button', { name: 'Continue' }).click()
verifyConfigFile(`cypress.config.js`)
})
})
})

View File

@@ -1,6 +1,7 @@
import { EnvironmentSetupFragmentDoc } from '../generated/graphql-test'
import EnvironmentSetup from './EnvironmentSetup.vue'
import { FRONTEND_FRAMEWORKS, CODE_LANGUAGES } from '../../../types/src/constants'
import { CODE_LANGUAGES } from '../../../types/src/constants'
import { FRONTEND_FRAMEWORKS } from '@packages/scaffold-config'
describe('<EnvironmentSetup />', { viewportWidth: 800 }, () => {
it('default component', () => {
@@ -28,6 +29,10 @@ describe('<EnvironmentSetup />', { viewportWidth: 800 }, () => {
return 'react-logo'
}
if (frameworkName.includes('Nuxt')) {
return 'nuxtjs-logo'
}
if (frameworkName.includes('Vue')) {
return 'vue-logo'
}
@@ -69,7 +74,7 @@ describe('<EnvironmentSetup />', { viewportWidth: 800 }, () => {
expanded: false,
}).click()
cy.findByRole('option', { name: 'Create React App (detected)' }).should('be.visible')
cy.findByRole('option', { name: 'Create React App (v4) (detected)' }).should('be.visible')
})
it('shows the description of bundler as Dev Server', () => {

View File

@@ -1,7 +1,7 @@
import { defaultMessages } from '@cy/i18n'
import InstallDependencies from './InstallDependencies.vue'
import { InstallDependenciesFragmentDoc } from '../generated/graphql-test'
import { PACKAGES_DESCRIPTIONS } from '../../../types/src/constants'
import { CYPRESS_REACT_LATEST, CYPRESS_WEBPACK } from '@packages/scaffold-config'
describe('<InstallDependencies />', () => {
beforeEach(function () {
@@ -24,8 +24,8 @@ describe('<InstallDependencies />', () => {
.should('be.visible')
.and('have.attr', 'href', 'https://www.npmjs.com/package/@cypress/webpack-dev-server')
cy.contains(PACKAGES_DESCRIPTIONS['@cypress/react'].split('<span')[0])
cy.contains(PACKAGES_DESCRIPTIONS['@cypress/webpack-dev-server'].split('<span')[0])
cy.contains(CYPRESS_REACT_LATEST.description.split('<span')[0])
cy.contains(CYPRESS_WEBPACK.description.split('<span')[0])
cy.percySnapshot()
})

View File

@@ -1,6 +1,6 @@
import { ManualInstallFragmentDoc } from '../generated/graphql-test'
import ManualInstall from './ManualInstall.vue'
import { PACKAGES_DESCRIPTIONS } from '../../../types/src/constants'
import { CYPRESS_REACT_LATEST, CYPRESS_WEBPACK } from '@packages/scaffold-config'
describe('<ManualInstall />', () => {
it('playground', () => {
@@ -14,8 +14,8 @@ describe('<ManualInstall />', () => {
})
it('lists packages and can copy install command to clipboard', { viewportWidth: 800, viewportHeight: 600 }, () => {
const framework = '@cypress/react'
const bundler = '@cypress/webpack-dev-server'
const framework = CYPRESS_REACT_LATEST
const bundler = CYPRESS_WEBPACK
cy.mountFragment(ManualInstallFragmentDoc, {
render: (gqlVal) => (
@@ -25,7 +25,7 @@ describe('<ManualInstall />', () => {
),
})
const installCommand = `yarn add -D ${framework} ${bundler}`
const installCommand = `npm install -D @cypress/react @cypress/webpack-dev-server`
// @ts-ignore
cy.findByRole('button', { name: 'Copy' }).realClick()
@@ -50,15 +50,15 @@ describe('<ManualInstall />', () => {
.invoke('readText')
.should('equal', installCommand)
const validatePackage = (packageName) => {
const validatePackage = (packageName: string) => {
cy.findByRole('link', { name: packageName })
.should('have.attr', 'href', `https://www.npmjs.com/package/${packageName}`)
cy.contains(PACKAGES_DESCRIPTIONS[framework].split('<span')[0])
cy.contains(framework.description.split('<span')[0])
}
validatePackage(framework)
validatePackage(bundler)
validatePackage(framework.package)
validatePackage(bundler.package)
})
it('flags packages already installed', () => {

View File

@@ -1,14 +1,15 @@
<template>
<TerminalPrompt
v-if="props.gql.wizard.installDependenciesCommand"
class="m-24px"
:command="installDependenciesCode"
:command="props.gql.wizard.installDependenciesCommand"
:project-folder-name="projectFolder"
/>
<div class="border-t border-t-gray-100 px-24px">
<ul>
<li
v-for="dep in props.gql.wizard.packagesToInstall"
:key="dep.id"
:key="dep.package"
class="border-b border-b-gray-100 py-16px last-of-type:border-b-0"
>
<i-cy-status-download-done_x24
@@ -55,6 +56,7 @@ fragment ManualInstall on Query {
description
package
}
installDependenciesCommand
}
currentProject {
id
@@ -70,19 +72,4 @@ const props = defineProps<{
gql: ManualInstallFragment
packagesInstalled: string[]
}>()
const commands = {
'npm': 'npm install -D ',
'pnpm': 'pnpm install -D ',
'yarn': 'yarn add -D ',
}
const installDependenciesCode = computed(
() => {
return commands[props.gql.currentProject?.packageManager ?? 'npm'] +
(props.gql.wizard.packagesToInstall ?? [])
.map((pack) => `${pack.package}`)
.join(' ')
},
)
</script>

View File

@@ -33,7 +33,7 @@ import {
} from '../generated/graphql'
import WarningList from '../warning/WarningList.vue'
import { computed } from 'vue'
import type { FrontendFramework, Bundler } from '@packages/types/src/constants'
import type { FrontendFramework, Bundler } from '@packages/scaffold-config'
import LaunchpadHeader from './LaunchpadHeader.vue'
import { useI18n } from '@cy/i18n'
import SelectLanguage from './SelectLanguage.vue'
@@ -41,7 +41,7 @@ import { useMutation } from '@urql/vue'
import WizardLayout from './WizardLayout.vue'
export interface WizardSetupData {
bundler: Bundler['type'] | null
bundler: Bundler | null
framework: FrontendFramework['type'] | null
codeLanguage: CodeLanguageEnum
}

View File

@@ -1,13 +1,12 @@
import { ref } from 'vue'
import SelectFwOrBundler from './SelectFwOrBundler.vue'
import SelectFwOrBundler, { Option } from './SelectFwOrBundler.vue'
const manyOptions = [
const manyOptions: Readonly<Option[]> = [
{
name: 'Vue.js',
id: 'vue',
isSelected: false,
type: 'vue',
category: 'vue',
type: 'vue2',
isDetected: true,
},
{
@@ -16,7 +15,6 @@ const manyOptions = [
id: 'react',
isSelected: false,
type: 'react',
category: 'react',
},
] as const
@@ -65,7 +63,7 @@ describe('<SelectFwOrBundler />', () => {
name: 'VueJs',
id: 'vue',
isSelected: false,
type: 'vuecli',
type: 'vueclivue3',
},
]}
/>
@@ -107,7 +105,7 @@ describe('<SelectFwOrBundler />', () => {
cy.mount(() => (
<div>
<div>click out</div>
<SelectFwOrBundler selectorType="framework" label="Front-end Framework" options={manyOptions} value="vue" />
<SelectFwOrBundler selectorType="framework" label="Front-end Framework" options={manyOptions} value="vue2" />
</div>
))

View File

@@ -8,12 +8,16 @@ import LogoReact from '../images/logos/react.svg'
import type { FrontendFrameworkEnum, SupportedBundlers } from '../generated/graphql'
export const FrameworkBundlerLogos: Record<FrontendFrameworkEnum | SupportedBundlers, string> = {
webpack: LogoWebpack,
webpack4: LogoWebpack,
webpack5: LogoWebpack,
vite: LogoVite,
vue: LogoVue,
vuecli: LogoVue,
vue2: LogoVue,
vue3: LogoVue,
vueclivue2: LogoVue,
vueclivue3: LogoVue,
nextjs: LogoNext,
nuxtjs: LogoNuxt,
react: LogoReact,
cra: LogoReact,
crav4: LogoReact,
crav5: LogoReact,
}

View File

@@ -0,0 +1,39 @@
## @packages/scaffold-config
Logic related to scaffolding new projects using launchpad, including detecting component frameworks and bundlers, installing dependencies and creating `cypress.config.js` files.
We have integrations for popular code generators like Create React App, Next.js and Vue CLI, and ship a `cypress.config.js` that will work out of the box for those templates.
We will also attempt to scaffold a configuration file for projects using React and Vue projects using Vite and Webpack that are not necessarily created using a code generator.
### Supported Frameworks and Libraries
| Name | Tool Version | Dev Server | Dev Server Version | Library Version (s) | Component Adaptor Version | Example Project
| --- | ---- | ---- | --- | ---- | ---- | --- |
| Create React App | 5.x | Webpack | 5.x | React 16, 17 | `@cypress/react@latest` | [TODO]
| Create React App | 4.x | Webpack | 4.x | React 16, React 17 | `@cypress/react@latest` | [Link](../../system-tests/projects/create-react-app-configured)
| React | - | Vite | 2.x | React 16, React 17 | `@cypress/react@latest` | [Link](../../system-tests/projects/react-vite-ts-configured)
| Vue | - | Vite | 2.x | Vue 3 | `@cypress/vue@^3.0.0` | [Link](../../system-tests/projects/vue3-vite-ts-configured)
| Vue CLI | 4.x | Webpack | 4.x | Vue 2 | `@cypress/vue@2.0.4` | [Link](../../system-tests/projects/vueclivue2-configured)
| Vue CLI | 4.x | Webpack | 4.x | Vue 3 | `@cypress/vue@latest` | [Link](../../system-tests/projects/vueclivue3-configured)
| Nuxt.js | 2.x | Webpack | 4.x, 5.x | Vue 2 | `@cypress/vue@2.0.4` | [Link](../../system-tests/projects/pristine-nuxtjs-vue2-configured)
### Adding More Projects
The process for adding a new library/framework/bundler is as follows:
1. Add your framework in [`src/frameworks.ts`](./src/frameworks.ts).
2. Any new dependencies are declared in [`src/constants.ts`](./src/constants.ts). Don't forget to add a description.
3. Ensure your project has the correct library and bundler detected with a test in [`test/integration/detect.ts`](./test/integration/scaffold-config.spec.ts)
3. Add a new project with the correct `cypress.config.js` and `package.json` to [system-tests/projects](../../system-tests/projects). It should be `<name>-configured`, which is a working example with some specs. Ensure it will run on CI by adding it to [`component_testing_spec.ts`](../../system-tests/test/component_testing_spec.ts).
4. Add another project called `<name>-unconfigured`, which represents the project prior to having Cypress added. This will be used in step 5.
5. Add a test to [`scaffold-component-testing.cy.ts`](../launchpad/cypress/e2e/scaffold-component-testing.cy.ts) to ensure your project has the correct `cypress.config.js` generated. Use an existing test as a template.
### TODO
These should be supported but currently are not configured.
| Name | Tool Version | Dev Server | Dev Server Version | Library Version (s) | Component Adaptor Version | Example Project
| --- | ---- | ---- | --- | ---- | ---- | --- |
| Next.js | 11.x, 12.x | Webpack | 4.x, 5.x | React 16, 17 | `@cypress/react@latest` | [Link](../../system-tests/projects/nextjs-configured)
| Vue CLI | 5.x | Webpack | 5.x | Vue 3 | `@cypress/vue@^3.0.0` | [TODO]

View File

@@ -0,0 +1,5 @@
if (process.env.CYPRESS_INTERNAL_ENV !== 'production') {
require('@packages/ts/register')
}
module.exports = require('./src')

View File

@@ -0,0 +1,26 @@
{
"name": "@packages/scaffold-config",
"version": "0.0.0-development",
"description": "Logic related to scaffolding new projects using launchpad",
"main": "index.js",
"browser": "src/index.ts",
"scripts": {
"build-prod": "tsc || echo 'built, with errors'",
"check-ts": "tsc --noEmit",
"clean": "rm -f ./src/*.js ./src/**/*.js ./src/**/**/*.js ./test/**/*.js || echo 'cleaned'",
"clean-deps": "rm -rf node_modules",
"test-unit": "mocha -r @packages/ts/register 'test/unit/**' --config ./test/.mocharc.js --exit"
},
"dependencies": {
"compare-versions": "4.1.3"
},
"devDependencies": {
"@packages/ts": "0.0.0-development",
"chai": "4.2.0",
"mocha": "7.0.1"
},
"files": [
"src"
],
"types": "src/index.ts"
}

View File

@@ -0,0 +1,154 @@
export const CODE_GEN_FRAMEWORKS = ['react', 'vue'] as const
export const FRONTEND_FRAMEWORK_CATEGORIES = ['react', 'vue'] as const
export const STORYBOOK_REACT = {
type: 'storybook',
name: ' Testing React',
package: '@storybook/testing-react',
installer: '@storybook/testing-react@latest',
description: 'Testing utilities that allow you to reuse your stories in your unit tests',
} as const
export const STORYBOOK_VUE = {
type: 'storybook',
name: ' Testing Vue 3',
package: '@storybook/testing-vue3',
installer: '@storybook/testing-vue3@latest',
description: 'Testing utilities that allow you to reuse your stories in your unit tests',
} as const
export const STORYBOOK_DEPS = [
STORYBOOK_REACT,
STORYBOOK_VUE,
] as const
export const CYPRESS_VUE_2 = {
type: 'cypress-adapter',
name: 'Cypress Vue',
package: '@cypress/vue',
installer: '@cypress/vue@^2.0.0',
description: 'Allows Cypress to mount each Vue component using <span class="text-purple-400">cy.mount()</span>',
} as const
export const CYPRESS_VUE_3 = {
type: 'cypress-adapter',
name: 'Cypress Vue',
package: '@cypress/vue',
installer: '@cypress/vue@^3.0.0',
description: 'Allows Cypress to mount each Vue component using <span class="text-purple-400">cy.mount()</span>',
} as const
export const WEBPACK_DEV_SERVER_4 = {
type: 'dev-server',
name: 'Webpack Dev Server',
package: 'webpack-dev-server',
installer: 'webpack-dev-server@^4.0.0',
description: 'Webpack Dev Server to bundle and run your tests',
} as const
export const HTML_WEBPACK_PLUGIN_4 = {
type: 'other',
name: 'HTML Webpack Plugin',
package: 'html-webpack-plugin',
installer: 'html-webpack-plugin@^4.0.0',
description: 'The HtmlWebpackPlugin simplifies creation of HTML files to serve your webpack bundles',
} as const
export const CYPRESS_WEBPACK = {
type: 'webpack',
name: 'Cypress Webpack Dev Server',
package: '@cypress/webpack-dev-server',
installer: '@cypress/webpack-dev-server@latest',
description: 'Allows Cypress to use your existing build configuration in order to bundle and run your tests',
} as const
export const CYPRESS_VITE = {
type: 'vite',
name: 'Cypress Vite Dev Server',
package: '@cypress/vite-dev-server',
installer: '@cypress/vite-dev-server@latest',
description: 'Allows Cypress to use your existing build configuration in order to bundle and run your tests',
} as const
export const CYPRESS_DEV_SERVERS = [
CYPRESS_WEBPACK,
CYPRESS_VITE,
] as const
export const BUNDLER_WEBPACK_4 = {
type: 'webpack4',
name: 'Webpack (v4)',
package: 'webpack',
installer: 'webpack@^4.0.0',
description: 'Webpack is a module bundler',
dependencies: [
CYPRESS_WEBPACK,
WEBPACK_DEV_SERVER_4,
HTML_WEBPACK_PLUGIN_4,
],
} as const
export const HTML_WEBPACK_PLUGIN_5 = {
type: 'other',
name: 'HTML Webpack Plugin',
package: 'html-webpack-plugin',
installer: 'html-webpack-plugin@^5.0.0',
description: 'The HtmlWebpackPlugin simplifies creation of HTML files to serve your webpack bundles',
} as const
export const BUNDLER_WEBPACK_5 = {
type: 'webpack5',
name: 'Webpack (v5)',
package: 'webpack',
installer: 'webpack@^5.0.0',
description: 'Webpack is a module bundler',
dependencies: [
CYPRESS_WEBPACK,
WEBPACK_DEV_SERVER_4,
HTML_WEBPACK_PLUGIN_5,
],
} as const
export const BUNDLER_VITE = {
type: 'vite',
name: 'Vite',
package: 'vite',
installer: 'vite@^2.0.0',
description: 'Vite is dev server that serves your source files over native ES modules',
dependencies: [CYPRESS_VITE],
} as const
export const BUNDLERS = [
BUNDLER_WEBPACK_4,
BUNDLER_WEBPACK_5,
BUNDLER_VITE,
]
export const CYPRESS_REACT_LATEST = {
type: 'cypress-adapter',
name: 'Cypress React',
package: '@cypress/react',
installer: '@cypress/react@^5.0.0',
description: 'Allows Cypress to mount each React component using <span class="text-purple-400">cy.mount()</span>',
} as const
export const CYPRESS_ADAPTER_DEPS = [
CYPRESS_REACT_LATEST,
CYPRESS_VUE_2,
CYPRESS_VUE_3,
] as const
export const DEV_SERVERS = [
WEBPACK_DEV_SERVER_4,
]
export const DEPENDENCIES = [
...CYPRESS_ADAPTER_DEPS,
...CYPRESS_DEV_SERVERS,
...STORYBOOK_DEPS,
...DEV_SERVERS,
...BUNDLERS,
HTML_WEBPACK_PLUGIN_4,
HTML_WEBPACK_PLUGIN_5,
] as const

View File

@@ -0,0 +1,106 @@
import { satisfies } from 'compare-versions'
import type { Bundler, PkgJson, FrontendFramework } from './types'
import { FRONTEND_FRAMEWORKS } from './frameworks'
export interface Detector {
version: string
dependency: string
}
interface DetectFramework {
framework?: FrontendFramework
bundler?: Bundler
}
const bundlers = [
{
type: 'vite',
detectors: [
{
dependency: 'vite',
version: '>=2.0.0',
},
],
},
{
type: 'webpack4',
detectors: [
{
dependency: 'webpack',
version: '^4.0.0',
},
],
},
{
type: 'webpack5',
detectors: [
{
dependency: 'webpack',
version: '^5.0.0',
},
],
},
] as const
// Detect the framework, which can either be a tool like Create React App,
// in which case we just return the framework. The user cannot change the
// bundler.
// If we don't find a specific framework, but we do find a library and/or
// bundler, we return both the framework, which might just be "React",
// and the bundler, which could be Vite.
export function detect (pkg: PkgJson): DetectFramework {
const inPkgJson = (detector: Detector) => {
const vers = pkg.dependencies?.[detector.dependency] || pkg.devDependencies?.[detector.dependency]
const found = (vers && satisfies(vers, detector.version)) ?? false
return found
}
// first see if it's a template
for (const framework of FRONTEND_FRAMEWORKS.filter((x) => x.family === 'template')) {
const hasAllDeps = [...framework.detectors].every(inPkgJson)
// so far all the templates we support only have 1 bundler,
// for example CRA only works with webpack,
// but we want to consider in the future, tools like Nuxt ship
// both a webpack and vite dev-env.
// if we support this, we will also need to attempt to infer the dev server of choice.
if (hasAllDeps && framework.supportedBundlers.length === 1) {
return {
framework,
}
}
}
// if not a template, they probably just installed/configured on their own.
for (const library of FRONTEND_FRAMEWORKS.filter((x) => x.family === 'library')) {
// multiple bundlers supported, eg React works with webpack and Vite.
// try to infer which one they are using.
const hasLibrary = [...library.detectors].every(inPkgJson)
for (const bundler of bundlers) {
const hasBundler = [...bundler.detectors].every(inPkgJson)
if (hasLibrary && hasBundler) {
return {
framework: library,
bundler: bundler.type,
}
}
}
if (hasLibrary) {
// unknown bundler, or we couldn't detect it
// just return the framework, leave the rest to the user.
return {
framework: library,
}
}
}
return {
framework: undefined,
bundler: undefined,
}
}

View File

@@ -0,0 +1,572 @@
import dedent from 'dedent'
import type { Bundler } from './types'
import {
CODE_GEN_FRAMEWORKS,
FRONTEND_FRAMEWORK_CATEGORIES,
CYPRESS_REACT_LATEST,
CYPRESS_VUE_2,
CYPRESS_VUE_3,
STORYBOOK_REACT,
STORYBOOK_VUE,
BUNDLER_WEBPACK_4,
} from './constants'
import { BUNDLER_VITE, BUNDLER_WEBPACK_5 } from '.'
const isWebpack = (bundler: Bundler) => ['webpack4', 'webpack5'].includes(bundler)
export const FRONTEND_FRAMEWORKS = [
{
type: 'crav4',
family: 'template',
name: 'Create React App (v4)',
supportedBundlers: [BUNDLER_WEBPACK_4],
packages: [
CYPRESS_REACT_LATEST,
BUNDLER_WEBPACK_4,
],
defaultPackagePath: '@cypress/react/plugins/react-scripts',
glob: '*.{js,jsx,tsx}',
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_REACT,
detectors: [
{
dependency: 'react-scripts',
version: '^4.0.0',
},
],
config: {
js: () => {
return dedent`
const { devServer } = require('@cypress/react/plugins/react-scripts')
module.exports = {
component: {
devServer,
}
}`
},
ts: () => {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/react/plugins/react-scripts'
export default defineConfig({
component: {
devServer,
}
})`
},
},
},
{
type: 'crav5',
family: 'template',
name: 'Create React App (v5)',
supportedBundlers: [BUNDLER_WEBPACK_5],
packages: [
CYPRESS_REACT_LATEST,
BUNDLER_WEBPACK_5,
],
defaultPackagePath: '@cypress/react/plugins/react-scripts',
glob: '*.{js,jsx,tsx}',
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_REACT,
detectors: [
{
dependency: 'react-scripts',
version: '^5.0.0',
},
],
config: {
js: () => {
return dedent`
const { devServer } = require('@cypress/react/plugins/react-scripts')
module.exports = {
component: {
devServer,
}
}`
},
ts: () => {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/react/plugins/react-scripts'
export default defineConfig({
component: {
devServer,
}
})`
},
},
},
{
type: 'vueclivue2',
name: 'Vue CLI (Vue 2)',
family: 'template',
supportedBundlers: [BUNDLER_WEBPACK_4],
packages: [
CYPRESS_VUE_2,
BUNDLER_WEBPACK_4,
],
defaultPackagePath: null,
glob: '*.vue',
detectors: [
{
dependency: '@vue/cli-service',
version: '^4.0.0',
},
{
dependency: 'vue',
version: '^2.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_VUE,
config: {
js: (bundler: Bundler) => {
return dedent`
const { devServer } = require('@cypress/webpack-dev-server')
const webpackConfig = require('@vue/cli-service/webpack.config')
module.exports = {
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
}
`
},
ts: () => {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import webpackConfig from '@vue/cli-service/webpack.config'
export default defineConfig({
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
})`
},
},
},
{
type: 'vueclivue3',
name: 'Vue CLI (Vue 3)',
family: 'template',
supportedBundlers: [BUNDLER_WEBPACK_4],
packages: [
CYPRESS_VUE_3,
BUNDLER_WEBPACK_4,
],
defaultPackagePath: null,
glob: '*.vue',
detectors: [
{
dependency: '@vue/cli-service',
version: '^4.0.0',
},
{
dependency: 'vue',
version: '^3.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_VUE,
config: {
js: (bundler: Bundler) => {
return dedent`
const { devServer } = require('@cypress/webpack-dev-server')
const webpackConfig = require('@vue/cli-service/webpack.config')
module.exports = {
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
}
`
},
ts: () => {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import webpackConfig from '@vue/cli-service/webpack.config'
export default defineConfig({
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
})`
},
},
},
{
type: 'react',
name: 'React.js',
family: 'library',
supportedBundlers: [BUNDLER_WEBPACK_4, BUNDLER_WEBPACK_5, BUNDLER_VITE],
packages: [CYPRESS_REACT_LATEST],
defaultPackagePath: null,
glob: '*.{js,jsx,tsx}',
detectors: [
{
dependency: 'react',
version: '>=16.0.0',
},
{
dependency: 'react-dom',
version: '>=16.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_REACT,
config: {
js: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
const { devServer } = require('@cypress/webpack-dev-server')
// NOTE: ensure you are requiring your webpack config from the
// correct location.
const webpackConfig = require('./webpack.config.js')
module.exports = {
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
}`
}
if (bundler === 'vite') {
return dedent`
const { devServer } = require('@cypress/vite-dev-server')
module.exports = {
component: {
devServer,
devServerConfig: {
// optionally provide your Vite config overrides.
}
}
}`
}
throw Error(`No config defined for ${bundler}`)
},
ts: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
// NOTE: ensure you are requiring your webpack config from the
// correct location.
import webpackConfig from './webpack.config.js'
export default defineConfig({
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
})`
}
if (bundler === 'vite') {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/vite-dev-server'
export default defineConfig({
component: {
devServer,
// optionally provide your Vite config overrides.
devServerConfig: {}
}
})`
}
throw Error(`No config defined for ${bundler}`)
},
},
},
{
type: 'vue2',
name: 'Vue.js (v2)',
family: 'library',
supportedBundlers: [BUNDLER_WEBPACK_4, BUNDLER_WEBPACK_5, BUNDLER_VITE],
packages: [CYPRESS_VUE_2],
defaultPackagePath: null,
glob: '*.vue',
detectors: [
{
dependency: 'vue',
version: '^2.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: null,
config: {
js: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
const { devServer } = require('@cypress/webpack-dev-server')
// NOTE: ensure you are requiring your webpack config from the
// correct location.
const webpackConfig = require('./webpack.config.js')
module.exports = {
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
}`
}
if (bundler === 'vite') {
return dedent`
const { devServer } = require('@cypress/vite-dev-server')
module.exports = {
component: {
devServer,
devServerConfig: {
// optionally provide your Vite config overrides.
}
}
}`
}
throw Error(`No config defined for ${bundler}`)
},
ts: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import webpackConfig from '@vue/cli-service/webpack.config'
export default defineConfig({
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
})`
}
if (bundler === 'vite') {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/vite-dev-server'
export default defineConfig({
component: {
devServer,
// optionally provide your Vite config overrides.
devServerConfig: {}
}
})`
}
throw Error(`No config defined for ${bundler}`)
},
},
},
{
type: 'vue3',
name: 'Vue.js (v3)',
family: 'library',
supportedBundlers: [BUNDLER_WEBPACK_4, BUNDLER_WEBPACK_5, BUNDLER_VITE],
packages: [CYPRESS_VUE_3],
defaultPackagePath: null,
glob: '*.vue',
detectors: [
{
dependency: 'vue',
version: '^3.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_VUE,
config: {
js: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
const { devServer } = require('@cypress/webpack-dev-server')
// NOTE: ensure you are requiring your webpack config from the
// correct location.
const webpackConfig = require('./webpack.config.js')
module.exports = {
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
}`
}
if (bundler === 'vite') {
return dedent`
const { devServer } = require('@cypress/vite-dev-server')
module.exports = {
component: {
devServer,
devServerConfig: {
// optionally provide your Vite config overrides.
}
}
}`
}
throw Error(`No config defined for ${bundler}`)
},
ts: (bundler: Bundler) => {
if (isWebpack(bundler)) {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import webpackConfig from '@vue/cli-service/webpack.config'
export default defineConfig({
component: {
devServer,
devServerConfig: {
webpackConfig
}
}
})`
}
if (bundler === 'vite') {
return dedent`
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/vite-dev-server'
export default defineConfig({
component: {
devServer,
// optionally provide your Vite config overrides.
devServerConfig: {}
}
})`
}
throw Error(`No config defined for ${bundler}`)
},
},
},
{
type: 'nextjs',
name: 'Next.js',
family: 'template',
supportedBundlers: [BUNDLER_WEBPACK_4],
packages: [
CYPRESS_REACT_LATEST,
BUNDLER_WEBPACK_4,
],
defaultPackagePath: '@cypress/react/plugins/next',
glob: '*.{js,jsx,tsx}',
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
detectors: [
{
dependency: 'next',
version: '>=10.0.0',
},
],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_REACT,
config: {
js: () => ``,
ts: () => ``,
},
},
{
type: 'nuxtjs',
name: 'Nuxt.js (v2)',
family: 'template',
supportedBundlers: [BUNDLER_WEBPACK_4],
packages: [
CYPRESS_VUE_2,
BUNDLER_WEBPACK_4,
],
defaultPackagePath: null,
glob: '*.vue',
detectors: [
{
dependency: 'nuxt',
version: '^2.0.0',
},
],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: null,
config: {
js: () => {
return dedent`
const { defineConfig } = require("cypress")
const { devServer } = require("@cypress/webpack-dev-server")
const { getWebpackConfig } = require("nuxt")
module.exports = defineConfig({
component: {
async devServer(cypressDevServerConfig) {
const webpackConfig = await getWebpackConfig()
return devServer(cypressDevServerConfig, { webpackConfig })
}
}
})`
},
ts: () => {
return dedent`
import { defineConfig } from "cypress"
import { devServer } from "@cypress/webpack-dev-server"
import { getWebpackConfig } from "nuxt"
export default defineConfig({
component: {
async devServer(cypressDevServerConfig) {
const webpackConfig = await getWebpackConfig()
return devServer(cypressDevServerConfig, { webpackConfig })
}
}
})`
},
},
},
] as const

View File

@@ -0,0 +1,7 @@
/* eslint-disable padding-line-between-statements */
// created by autobarrel, do not modify directly
export * from './constants'
export * from './detect'
export * from './frameworks'
export * from './types'

View File

@@ -0,0 +1,16 @@
import type { BUNDLERS, STORYBOOK_DEPS, CODE_GEN_FRAMEWORKS, DEPENDENCIES } from './constants'
import type { FRONTEND_FRAMEWORKS } from './frameworks'
export type AllPackages = FrontendFramework['packages'][number]
| typeof BUNDLERS[number]['package']
| typeof STORYBOOK_DEPS[number]
export type AllPackagePackages = typeof DEPENDENCIES[number]['package']
export type Bundler = typeof BUNDLERS[number]['type']
export type CodeGenFramework = typeof CODE_GEN_FRAMEWORKS[number]
export type FrontendFramework = typeof FRONTEND_FRAMEWORKS[number]
export type PkgJson = { dependencies?: Record<string, string>, devDependencies?: Record<string, string> }

View File

@@ -0,0 +1 @@
module.exports = {}

View File

@@ -0,0 +1,95 @@
import { expect } from 'chai'
import { e2eProjectDirs } from '@packages/frontend-shared/cypress/e2e/support/e2eProjectDirs'
import { detect } from '../../src'
import Fixtures from '@tooling/system-tests/lib/fixtures'
import path from 'path'
import fs from 'fs-extra'
export async function scaffoldMigrationProject (project: typeof e2eProjectDirs[number]) {
Fixtures.removeProject(project)
await Fixtures.scaffoldProject(project)
return fs.readJSONSync(path.join(Fixtures.projectPath(project), 'package.json'))
}
describe('detect', () => {
it('Create React App v4', async () => {
const pkg = await scaffoldMigrationProject('create-react-app-unconfigured')
const actual = detect({ ...pkg, dependencies: { ...pkg.dependencies, 'react-scripts': '4.0.0' } })
expect(actual.framework.type).to.eq('crav4')
})
it('Create React App v5', async () => {
const pkg = await scaffoldMigrationProject('create-react-app-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('crav5')
})
it('React App with webpack 5', async () => {
const pkg = await scaffoldMigrationProject('react-app-webpack-5-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('react')
expect(actual.bundler).to.eq('webpack5')
})
it(`Vue CLI w/ Vue 2`, async () => {
const pkg = await scaffoldMigrationProject('vueclivue2-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('vueclivue2')
})
it(`Vue CLI w/ Vue 3`, async () => {
const pkg = await scaffoldMigrationProject('vueclivue3-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('vueclivue3')
})
it(`React with Vite`, async () => {
const pkg = await scaffoldMigrationProject('react-vite-ts-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('react')
expect(actual.bundler).to.eq('vite')
})
it(`Vue with Vite`, async () => {
const pkg = await scaffoldMigrationProject('vue3-vite-ts-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('vue3')
expect(actual.bundler).to.eq('vite')
})
it(`Next.js`, async () => {
const pkg = await scaffoldMigrationProject('nextjs-unconfigured')
const actual = detect(pkg)
expect(actual.framework.type).to.eq('nextjs')
expect(actual.bundler).to.eq(undefined)
})
;['10', '11', '12'].forEach((v) => {
it(`Next.js v${v}`, async () => {
const pkg = await scaffoldMigrationProject('nextjs-unconfigured')
const actual = detect({ ...pkg, dependencies: {
'next': v,
} })
expect(actual.framework.type).to.eq('nextjs')
expect(actual.bundler).to.eq(undefined)
})
})
it(`no framework or library`, async () => {
const actual = detect({})
expect(actual.framework).to.be.undefined
expect(actual.bundler).to.be.undefined
})
})

View File

@@ -0,0 +1,20 @@
{
"extends": "../ts/tsconfig.json",
"include": ["src"],
"exclude": [
"test",
"script"
],
"compilerOptions": {
"lib": ["es2020"],
"strict": true,
"allowJs": false,
"noImplicitAny": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"noUnusedLocals": false,
"noUncheckedIndexedAccess": true,
"importsNotUsedAsValues": "error",
"types": [],
}
}

View File

@@ -2,109 +2,6 @@ export const PLUGINS_STATE = ['uninitialized', 'initializing', 'initialized', 'e
export type PluginsState = typeof PLUGINS_STATE[number]
export const BUNDLERS = [
{
type: 'webpack',
name: 'Webpack',
package: '@cypress/webpack-dev-server',
},
{
type: 'vite',
name: 'Vite',
package: '@cypress/vite-dev-server',
},
] as const
export type Bundler = typeof BUNDLERS[number]
export const CODE_GEN_FRAMEWORKS = ['react', 'vue'] as const
export type CodeGenFramework = typeof CODE_GEN_FRAMEWORKS[number]
export const FRONTEND_FRAMEWORK_CATEGORIES = ['react', 'vue'] as const
export const STORYBOOK_DEPS = [
'@storybook/testing-react',
'@storybook/testing-vue3',
] as const
export const FRONTEND_FRAMEWORKS = [
{
type: 'cra',
name: 'Create React App',
supportedBundlers: ['webpack'] as readonly Bundler['type'][],
package: '@cypress/react',
defaultPackagePath: '@cypress/react/plugins/react-scripts',
glob: '*.{jsx,tsx}',
deps: ['react-scripts', 'react', 'react-dom'],
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_DEPS[0],
},
{
type: 'vuecli',
name: 'Vue CLI',
supportedBundlers: ['webpack'] as readonly Bundler['type'][],
package: '@cypress/vue',
defaultPackagePath: null,
glob: '*.vue',
deps: ['@vue/cli-service', 'vue'],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_DEPS[1],
},
{
type: 'react',
name: 'React.js',
supportedBundlers: ['webpack', 'vite'] as readonly Bundler['type'][],
package: '@cypress/react',
defaultPackagePath: null,
glob: '*.{jsx,tsx}',
deps: ['react', 'react-dom'],
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_DEPS[0],
},
{
type: 'vue',
name: 'Vue.js',
supportedBundlers: ['webpack', 'vite'] as readonly Bundler['type'][],
package: '@cypress/vue',
defaultPackagePath: null,
glob: '*.vue',
deps: ['vue'],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_DEPS[1],
},
{
type: 'nextjs',
name: 'Next.js',
supportedBundlers: ['webpack'] as readonly Bundler['type'][],
package: '@cypress/react',
defaultPackagePath: '@cypress/react/plugins/next',
glob: '*.{jsx,tsx}',
deps: ['next', 'react', 'react-dom'],
category: FRONTEND_FRAMEWORK_CATEGORIES[0],
codeGenFramework: CODE_GEN_FRAMEWORKS[0],
storybookDep: STORYBOOK_DEPS[0],
},
{
type: 'nuxtjs',
name: 'Nuxt.js',
supportedBundlers: ['webpack'] as readonly Bundler['type'][],
package: '@cypress/vue',
defaultPackagePath: null,
glob: '*.vue',
deps: ['nuxt'],
category: FRONTEND_FRAMEWORK_CATEGORIES[1],
codeGenFramework: CODE_GEN_FRAMEWORKS[1],
storybookDep: STORYBOOK_DEPS[1],
},
] as const
export type FrontendFramework = typeof FRONTEND_FRAMEWORKS[number]
export const CODE_LANGUAGES = [
{
type: 'js',
@@ -120,17 +17,4 @@ export type CodeLanguage = typeof CODE_LANGUAGES[number]
export const MIGRATION_STEPS = ['renameAuto', 'renameManual', 'renameSupport', 'configFile', 'setupComponent'] as const
export type AllPackages = FrontendFramework['package'] | Bundler['package'] | typeof STORYBOOK_DEPS[number]
export type AllPackageTypes = FrontendFramework['type'] | Bundler['type']
export const PACKAGES_DESCRIPTIONS: Record<AllPackages, string> = {
'@cypress/vue': 'Allows Cypress to mount each Vue component using <span class="text-purple-400">cy.mount()</span>',
'@cypress/react': 'Allows Cypress to mount each React component using <span class="text-purple-400">cy.mount()</span>',
'@cypress/webpack-dev-server': 'Allows Cypress to use your existing build configuration in order to bundle and run your tests',
'@cypress/vite-dev-server': 'Allows Cypress to use your existing build configuration in order to bundle and run your tests',
'@storybook/testing-react': 'Testing utilities that allow you to reuse your stories in your unit tests',
'@storybook/testing-vue3': 'Testing utilities that allow you to reuse your stories in your unit tests',
} as const
export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'] as const

View File

@@ -14,11 +14,6 @@ export * from './editors'
export * from './auth'
export type {
AllPackages,
AllPackageTypes,
} from './constants'
export * from './browser'
export type { PlatformName } from './platform'

View File

@@ -27,6 +27,7 @@ export const monorepoPaths = {
pkgRunner: path.join(__dirname, '../../packages/runner'),
pkgRunnerCt: path.join(__dirname, '../../packages/runner-ct'),
pkgRunnerShared: path.join(__dirname, '../../packages/runner-shared'),
pkgScaffoldConfig: path.join(__dirname, '../../packages/scaffold-config'),
pkgServer: path.join(__dirname, '../../packages/server'),
pkgSocket: path.join(__dirname, '../../packages/socket'),
pkgTs: path.join(__dirname, '../../packages/ts'),

View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@@ -0,0 +1,7 @@
const { devServer } = require('@cypress/react/plugins/react-scripts')
module.exports = {
component: {
devServer,
}
}

View File

@@ -0,0 +1,36 @@
{
"name": "create-react-app-unconfigured",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"@cypress/react": "^5.0.0",
"@cypress/webpack-dev-server": "^1.8.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"html-webpack-plugin": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.0",
"webpack": "^4.0.0",
"webpack-dev-server": "^4.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,8 @@
import { mount } from '@cypress/react'
import React from 'react'
import App from './App'
it('works', () => {
mount(<App />)
cy.contains('Learn React')
})

View File

@@ -0,0 +1,25 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `yarn build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

View File

@@ -0,0 +1,33 @@
{
"name": "create-react-app-unconfigured",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"_cySkipYarnInstall": true
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,25 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Button } from './button'
it('works', () => {
mount(<Button />)
cy.get('button').contains('Hello World')
})

View File

@@ -0,0 +1,7 @@
import './button.module.css'
export const Button = () => {
return (
<button>Hello World</button>
)
}

View File

@@ -0,0 +1,4 @@
.button {
background: black;
color: white;
}

View File

@@ -0,0 +1,8 @@
const { devServer } = require('@cypress/react/plugins/next')
module.exports = {
component: {
devServer
}
}

View File

@@ -0,0 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack5: false,
reactStrictMode: true,
}
module.exports = nextConfig

View File

@@ -0,0 +1,24 @@
{
"name": "pristine-nextjs-configured",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^11.0.0",
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@cypress/react": "^5.0.0",
"@cypress/webpack-dev-server": "^1.0.0",
"eslint": "8.9.0",
"eslint-config-next": "12.1.0",
"html-webpack-plugin": "^4.0.0",
"webpack-dev-server": "^4.0.0"
}
}

View File

@@ -0,0 +1,7 @@
import '../styles/globals.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp

View File

@@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default function handler(req, res) {
res.status(200).json({ name: 'John Doe' })
}

View File

@@ -0,0 +1,69 @@
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,116 @@
.container {
padding: 0 2rem;
}
.main {
min-height: 100vh;
padding: 4rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
display: flex;
flex: 1;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
flex-grow: 1;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
margin: 4rem 0;
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
}
.card {
margin: 1rem;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 300px;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
margin-left: 0.5rem;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

View File

@@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Button } from './button'
it('works', () => {
mount(<Button />)
cy.get('button').contains('Hello World')
})

View File

@@ -0,0 +1,7 @@
import './button.module.css'
export const Button = () => {
return (
<button>Hello World</button>
)
}

View File

@@ -0,0 +1,4 @@
.button {
background: black;
color: white;
}

Some files were not shown because too many files have changed in this diff Show More