Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ system-tests/projects/create-react-app-custom-index-html

system-tests/projects/vueclivue2-unconfigured/**/*
system-tests/projects/vueclivue2-configured/**/*
system-tests/projects/outdated-deps-vuecli3/**/*

system-tests/projects/vueclivue3-unconfigured/**/*
system-tests/projects/vueclivue3-configured/**/*
Expand Down
79 changes: 78 additions & 1 deletion packages/data-context/src/data/ProjectConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { CypressEnv } from './CypressEnv'
import { autoBindDebug } from '../util/autoBindDebug'
import type { EventRegistrar } from './EventRegistrar'
import type { DataContext } from '../DataContext'
import { DependencyToInstall, inPkgJson, WIZARD_BUNDLERS, WIZARD_DEPENDENCIES, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'

const debug = debugLib(`cypress:lifecycle:ProjectConfigManager`)

Expand Down Expand Up @@ -153,8 +154,84 @@ export class ProjectConfigManager {
if (this._registeredEventsTarget && this._testingType !== this._registeredEventsTarget) {
this.options.refreshLifecycle().catch(this.onLoadError)
} else if (this._eventsIpc && !this._registeredEventsTarget && this._cachedLoadConfig) {
this.setupNodeEvents(this._cachedLoadConfig).catch(this.onLoadError)
this.setupNodeEvents(this._cachedLoadConfig)
.then(() => {
if (this._testingType === 'component') {
this.checkDependenciesForComponentTesting()
}
})
.catch(this.onLoadError)
}
}

checkDependenciesForComponentTesting () {
// if it's a function, for example, the user is created their own dev server,
// and not using one of our presets. Assume they know what they are doing and
// what dependencies they require.
if (typeof this._cachedLoadConfig?.initialConfig?.component?.devServer !== 'object') {
return
}

const devServerOptions = this._cachedLoadConfig.initialConfig.component.devServer

const bundler = WIZARD_BUNDLERS.find((x) => x.type === devServerOptions.bundler)

// Use a map since sometimes the same dependency can appear in `bundler` and `framework`,
// for example webpack appears in both `bundler: 'webpack', framework: 'react-scripts'`
const unsupportedDeps = new Map<DependencyToInstall['dependency']['type'], DependencyToInstall>()

if (!bundler) {
return
}

const result = inPkgJson(bundler, this.options.projectRoot)

if (!result.satisfied) {
unsupportedDeps.set(result.dependency.type, result)
}

const isFrameworkSatisfied = (bundler: typeof WIZARD_BUNDLERS[number], framework: typeof WIZARD_FRAMEWORKS[number]) => {
for (const dep of framework.dependencies(bundler.type, this.options.projectRoot)) {
const res = inPkgJson(dep.dependency, this.options.projectRoot)

if (!res.satisfied) {
return false
}
}

return true
}

const frameworks = WIZARD_FRAMEWORKS.filter((x) => x.configFramework === devServerOptions.framework)

const mismatchedFrameworkDeps = new Map<typeof WIZARD_DEPENDENCIES[number]['type'], DependencyToInstall>()

let isSatisfied = false

for (const framework of frameworks) {
if (isFrameworkSatisfied(bundler, framework)) {
isSatisfied = true
break
} else {
for (const dep of framework.dependencies(bundler.type, this.options.projectRoot)) {
mismatchedFrameworkDeps.set(dep.dependency.type, dep)
}
}
}

if (!isSatisfied) {
for (const dep of Array.from(mismatchedFrameworkDeps.values())) {
if (!dep.satisfied) {
unsupportedDeps.set(dep.dependency.type, dep)
}
}
}

if (unsupportedDeps.size === 0) {
return
}

this.options.ctx.onWarning(getError('COMPONENT_TESTING_MISMATCHED_DEPENDENCIES', Array.from(unsupportedDeps.values())))
}

private async setupNodeEvents (loadConfigReply: LoadConfigReply): Promise<void> {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions packages/errors/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { BreakingErrResult } from '@packages/config'
import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUtils'
import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate'
import { stackWithoutMessage } from './stackUtils'
import type { DependencyToInstall } from '@packages/scaffold-config'
import type { ClonedError, ConfigValidationFailureInfo, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes'

const ansi_up = new AU()
Expand Down Expand Up @@ -1561,6 +1562,24 @@ export const AllCypressErrors = {
https://on.cypress.io/configuration
`
},

COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: (dependencies: DependencyToInstall[]) => {
const deps = dependencies.map<string>((dep) => {
if (dep.detectedVersion) {
return `\`${dep.dependency.installer}\`. Expected ${dep.dependency.minVersion}, found ${dep.detectedVersion}.`
}

return `\`${dep.dependency.installer}\`. Expected ${dep.dependency.minVersion} but dependency was not found.`
})

return errTemplate`
We detected that you have versions of dependencies that are not officially supported:

${fmt.listItems(deps, { prefix: ' - ' })}

If you're experiencing problems, downgrade dependencies and restart Cypress.
`
},
} as const

// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down
22 changes: 22 additions & 0 deletions packages/errors/test/unit/visualSnapshotErrors_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,5 +1172,27 @@ describe('visual error templates', () => {
default: ['component'],
}
},

COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: () => {
return {
default: [
[
{
dependency: {
type: 'vite',
name: 'Vite',
package: 'vite',
installer: 'vite',
description: 'Vite is dev server that serves your source files over native ES modules',
minVersion: '>=2.0.0',
},
satisfied: false,
detectedVersion: '1.0.0',
loc: null,
},
],
],
}
},
})
})
12 changes: 12 additions & 0 deletions packages/frontend-shared/src/warning/Warning.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
>
<div
ref="markdownTarget"
class="warning-markdown"
v-html="markdown"
/>
<Button
Expand Down Expand Up @@ -68,3 +69,14 @@ let message = computed(() => {

const { markdown } = useMarkdown(markdownTarget, message.value, { classes: { code: ['bg-warning-200'] } })
</script>

<style lang="scss">
// Add some extra margin to the <ul>
// TODO: ideally move this into `frontend-shared/src/composables/useMarkdown`
// It doesn't get applied when added there due to conflicting with other, higher priority rules.
.warning-markdown {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to not use tailwind?

Copy link
Contributor Author

@lmiller1990 lmiller1990 Jul 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - I still need to do .warning-markdown > ul since you cannot nest selectors using Tailwind, but I changed it to use @apply.

The reason I did it like this is we need to style the <ul> which is part of the generated HTML in the errors package. I tried adding it with the rest of the styles but it's getting clobbered by something else and the styles aren't applied:
image

Updated the code leaving a comment - not sure if this ever will be addressed, but at least it'll point people to the right place if they want to expand on the styling.

ul {
@apply ml-16px mb-16px;
}
}
</style>
1 change: 1 addition & 0 deletions packages/graphql/schemas/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,7 @@ enum ErrorTypeEnum {
CDP_VERSION_TOO_OLD
CHROME_WEB_SECURITY_NOT_SUPPORTED
COMPONENT_FOLDER_REMOVED
COMPONENT_TESTING_MISMATCHED_DEPENDENCIES
CONFIG_FILES_LANGUAGE_CONFLICT
CONFIG_FILE_DEV_SERVER_INVALID_RETURN
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID
Expand Down
52 changes: 52 additions & 0 deletions packages/launchpad/cypress/e2e/config-warning.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,55 @@ describe('experimentalStudio', () => {
cy.get('[data-cy="warning-alert"]').contains('Warning: Experimental Studio Removed')
})
})

describe('component testing dependency warnings', () => {
it('warns against outdated react and vite version', () => {
cy.scaffoldProject('component-testing-outdated-dependencies')
cy.addProject('component-testing-outdated-dependencies')
cy.openGlobalMode()
cy.visitLaunchpad()
cy.contains('component-testing-outdated-dependencies').click()
cy.get('[data-cy="warning-alert"]').should('not.exist')
cy.get('a').contains('Projects').click()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-cy="warning-alert"]').should('exist')
.should('contain.text', 'Warning: Component Testing Mismatched Dependencies')
.should('contain.text', 'vite. Expected ^=2.0.0 || ^=3.0.0, found 2.0.0-beta.70')
.should('contain.text', 'react. Expected ^=16.0.0 || ^=17.0.0 || ^=18.0.0, found 15.6.2.')
.should('contain.text', 'react-dom. Expected ^=16.0.0 || ^=17.0.0 || ^=18.0.0 but dependency was not found.')

cy.get('.warning-markdown').find('li').should('have.length', 3)
})

it('warns against outdated @vue/cli dependency', () => {
cy.scaffoldProject('outdated-deps-vuecli3')
cy.addProject('outdated-deps-vuecli3')
cy.openGlobalMode()
cy.visitLaunchpad()
cy.contains('outdated-deps-vuecli3').click()
cy.get('[data-cy="warning-alert"]').should('not.exist')
cy.get('a').contains('Projects').click()
cy.get('[data-cy-testingtype="component"]').click()
cy.get('[data-cy="warning-alert"]').should('exist')
.should('contain.text', 'Warning: Component Testing Mismatched Dependencies')
.should('contain.text', '@vue/cli-service. Expected ^=4.0.0 || ^=5.0.0, found 3.12.1.')
.should('contain.text', 'vue. Expected ^3.0.0, found 2.7.8.')

cy.get('.warning-markdown').find('li').should('have.length', 2)
})

it('does not show warning for project with supported dependencies', () => {
cy.scaffoldProject('vueclivue3-configured')
cy.addProject('vueclivue3-configured')
cy.openGlobalMode()
cy.visitLaunchpad()
cy.contains('vueclivue3-configured').click()
cy.get('[data-cy="warning-alert"]').should('not.exist')
cy.get('a').contains('Projects').click()
cy.get('[data-cy-testingtype="component"]').click()

// Wait until launch browser screen and assert warning does not exist
cy.contains('Choose a Browser')
cy.get('[data-cy="warning-alert"]').should('not.exist')
})
})
14 changes: 7 additions & 7 deletions packages/scaffold-config/src/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const WIZARD_DEPENDENCY_WEBPACK = {
package: 'webpack',
installer: 'webpack',
description: 'Webpack is a module bundler',
minVersion: '>=4.0.0',
minVersion: '>=4.0.0 || >=5.0.0',
} as const

export const WIZARD_DEPENDENCY_VUE_2 = {
Expand All @@ -31,7 +31,7 @@ export const WIZARD_DEPENDENCY_REACT = {
package: 'react',
installer: 'react',
description: 'A JavaScript library for building user interfaces',
minVersion: '>=16.x',
minVersion: '^=16.0.0 || ^=17.0.0 || ^=18.0.0',
} as const

export const WIZARD_DEPENDENCY_REACT_DOM = {
Expand All @@ -40,7 +40,7 @@ export const WIZARD_DEPENDENCY_REACT_DOM = {
package: 'react-dom',
installer: 'react-dom',
description: 'This package serves as the entry point to the DOM and server renderers for React',
minVersion: '>=16.x',
minVersion: '^=16.0.0 || ^=17.0.0 || ^=18.0.0',
} as const

export const WIZARD_DEPENDENCY_TYPESCRIPT = {
Expand All @@ -58,7 +58,7 @@ export const WIZARD_DEPENDENCY_REACT_SCRIPTS = {
package: 'react-scripts',
installer: 'react-scripts',
description: 'Create React apps with no build configuration',
minVersion: '>=4.0.0',
minVersion: '^=4.0.0 || ^=5.0.0',
} as const

export const WIZARD_DEPENDENCY_VUE_CLI_SERVICE = {
Expand All @@ -67,7 +67,7 @@ export const WIZARD_DEPENDENCY_VUE_CLI_SERVICE = {
package: '@vue/cli-service',
installer: '@vue/cli-service',
description: 'Standard Tooling for Vue.js Development',
minVersion: '>=4.0.0',
minVersion: '^=4.0.0 || ^=5.0.0',
} as const

export const WIZARD_DEPENDENCY_VITE = {
Expand All @@ -76,7 +76,7 @@ export const WIZARD_DEPENDENCY_VITE = {
package: 'vite',
installer: 'vite',
description: 'Vite is dev server that serves your source files over native ES modules',
minVersion: '>=2.0.0',
minVersion: '^=2.0.0 || ^=3.0.0',
} as const

export const WIZARD_DEPENDENCY_NUXT = {
Expand All @@ -94,7 +94,7 @@ export const WIZARD_DEPENDENCY_NEXT = {
package: 'next',
installer: 'next',
description: 'The React Framework for Production',
minVersion: '>=10.0.0',
minVersion: '^=10.0.0 || ^=11.0.0 || ^=12.0.0',
} as const

export const WIZARD_DEPENDENCY_ANGULAR_CLI = {
Expand Down
9 changes: 6 additions & 3 deletions packages/scaffold-config/src/frameworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,20 @@ export function inPkgJson (dependency: WizardDependency, projectPath: string): D
// TODO: convert to async FS method
// eslint-disable-next-line no-restricted-syntax
const pkg = fs.readJsonSync(loc) as PkgJson
const pkgVersion = semver.coerce(pkg.version)

if (!pkgVersion) {
if (!pkg.version) {
throw Error(`${pkg.version} for ${dependency.package} is not a valid semantic version.`)
}

const satisfied = Boolean(pkg.version && semver.satisfies(pkg.version, dependency.minVersion, {
includePrerelease: true,
}))

return {
dependency,
detectedVersion: pkg.version,
loc,
satisfied: Boolean(pkg.version && semver.satisfies(pkgVersion, dependency.minVersion)),
satisfied,
}
} catch (e) {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
component: {
supportFile: false,
devServer: {
bundler: 'vite',
framework: 'react',
},
indexHtmlFile: 'cypress/component/support/component-index.html',
},
}
Loading