Skip to content

Commit cbb5e35

Browse files
feat: warn when detecting unsupported dependencies for component testing (#22964)
* wip: basic implementation * update dependencies to have maxVersion * handle promises correctly * fix test * update test project and styling * only check for CT deps in CT * install required deps * revert * rework detection and extend tests * remove unused code * remove more code Co-authored-by: Zachary Williams <ZachJW34@gmail.com>
1 parent 2612219 commit cbb5e35

File tree

22 files changed

+21141
-14
lines changed

22 files changed

+21141
-14
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ system-tests/projects/create-react-app-custom-index-html
8787

8888
system-tests/projects/vueclivue2-unconfigured/**/*
8989
system-tests/projects/vueclivue2-configured/**/*
90+
system-tests/projects/outdated-deps-vuecli3/**/*
9091

9192
system-tests/projects/vueclivue3-unconfigured/**/*
9293
system-tests/projects/vueclivue3-configured/**/*

packages/data-context/src/data/ProjectConfigManager.ts

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { CypressEnv } from './CypressEnv'
1111
import { autoBindDebug } from '../util/autoBindDebug'
1212
import type { EventRegistrar } from './EventRegistrar'
1313
import type { DataContext } from '../DataContext'
14+
import { DependencyToInstall, inPkgJson, WIZARD_BUNDLERS, WIZARD_DEPENDENCIES, WIZARD_FRAMEWORKS } from '@packages/scaffold-config'
1415

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

@@ -153,8 +154,84 @@ export class ProjectConfigManager {
153154
if (this._registeredEventsTarget && this._testingType !== this._registeredEventsTarget) {
154155
this.options.refreshLifecycle().catch(this.onLoadError)
155156
} else if (this._eventsIpc && !this._registeredEventsTarget && this._cachedLoadConfig) {
156-
this.setupNodeEvents(this._cachedLoadConfig).catch(this.onLoadError)
157+
this.setupNodeEvents(this._cachedLoadConfig)
158+
.then(() => {
159+
if (this._testingType === 'component') {
160+
this.checkDependenciesForComponentTesting()
161+
}
162+
})
163+
.catch(this.onLoadError)
164+
}
165+
}
166+
167+
checkDependenciesForComponentTesting () {
168+
// if it's a function, for example, the user is created their own dev server,
169+
// and not using one of our presets. Assume they know what they are doing and
170+
// what dependencies they require.
171+
if (typeof this._cachedLoadConfig?.initialConfig?.component?.devServer !== 'object') {
172+
return
173+
}
174+
175+
const devServerOptions = this._cachedLoadConfig.initialConfig.component.devServer
176+
177+
const bundler = WIZARD_BUNDLERS.find((x) => x.type === devServerOptions.bundler)
178+
179+
// Use a map since sometimes the same dependency can appear in `bundler` and `framework`,
180+
// for example webpack appears in both `bundler: 'webpack', framework: 'react-scripts'`
181+
const unsupportedDeps = new Map<DependencyToInstall['dependency']['type'], DependencyToInstall>()
182+
183+
if (!bundler) {
184+
return
185+
}
186+
187+
const result = inPkgJson(bundler, this.options.projectRoot)
188+
189+
if (!result.satisfied) {
190+
unsupportedDeps.set(result.dependency.type, result)
191+
}
192+
193+
const isFrameworkSatisfied = (bundler: typeof WIZARD_BUNDLERS[number], framework: typeof WIZARD_FRAMEWORKS[number]) => {
194+
for (const dep of framework.dependencies(bundler.type, this.options.projectRoot)) {
195+
const res = inPkgJson(dep.dependency, this.options.projectRoot)
196+
197+
if (!res.satisfied) {
198+
return false
199+
}
200+
}
201+
202+
return true
203+
}
204+
205+
const frameworks = WIZARD_FRAMEWORKS.filter((x) => x.configFramework === devServerOptions.framework)
206+
207+
const mismatchedFrameworkDeps = new Map<typeof WIZARD_DEPENDENCIES[number]['type'], DependencyToInstall>()
208+
209+
let isSatisfied = false
210+
211+
for (const framework of frameworks) {
212+
if (isFrameworkSatisfied(bundler, framework)) {
213+
isSatisfied = true
214+
break
215+
} else {
216+
for (const dep of framework.dependencies(bundler.type, this.options.projectRoot)) {
217+
mismatchedFrameworkDeps.set(dep.dependency.type, dep)
218+
}
219+
}
220+
}
221+
222+
if (!isSatisfied) {
223+
for (const dep of Array.from(mismatchedFrameworkDeps.values())) {
224+
if (!dep.satisfied) {
225+
unsupportedDeps.set(dep.dependency.type, dep)
226+
}
227+
}
157228
}
229+
230+
if (unsupportedDeps.size === 0) {
231+
return
232+
}
233+
234+
this.options.ctx.onWarning(getError('COMPONENT_TESTING_MISMATCHED_DEPENDENCIES', Array.from(unsupportedDeps.values())))
158235
}
159236

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

packages/errors/__snapshot-html__/COMPONENT_TESTING_MISMATCHED_DEPENDENCIES.html

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/errors/src/errors.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { BreakingErrResult } from '@packages/config'
99
import { humanTime, logError, parseResolvedPattern, pluralize } from './errorUtils'
1010
import { errPartial, errTemplate, fmt, theme, PartialErr } from './errTemplate'
1111
import { stackWithoutMessage } from './stackUtils'
12+
import type { DependencyToInstall } from '@packages/scaffold-config'
1213
import type { ClonedError, ConfigValidationFailureInfo, CypressError, ErrTemplateResult, ErrorLike } from './errorTypes'
1314

1415
const ansi_up = new AU()
@@ -1561,6 +1562,24 @@ export const AllCypressErrors = {
15611562
https://on.cypress.io/configuration
15621563
`
15631564
},
1565+
1566+
COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: (dependencies: DependencyToInstall[]) => {
1567+
const deps = dependencies.map<string>((dep) => {
1568+
if (dep.detectedVersion) {
1569+
return `\`${dep.dependency.installer}\`. Expected ${dep.dependency.minVersion}, found ${dep.detectedVersion}.`
1570+
}
1571+
1572+
return `\`${dep.dependency.installer}\`. Expected ${dep.dependency.minVersion} but dependency was not found.`
1573+
})
1574+
1575+
return errTemplate`
1576+
We detected that you have versions of dependencies that are not officially supported:
1577+
1578+
${fmt.listItems(deps, { prefix: ' - ' })}
1579+
1580+
If you're experiencing problems, downgrade dependencies and restart Cypress.
1581+
`
1582+
},
15641583
} as const
15651584

15661585
// eslint-disable-next-line @typescript-eslint/no-unused-vars

packages/errors/test/unit/visualSnapshotErrors_spec.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,5 +1172,27 @@ describe('visual error templates', () => {
11721172
default: ['component'],
11731173
}
11741174
},
1175+
1176+
COMPONENT_TESTING_MISMATCHED_DEPENDENCIES: () => {
1177+
return {
1178+
default: [
1179+
[
1180+
{
1181+
dependency: {
1182+
type: 'vite',
1183+
name: 'Vite',
1184+
package: 'vite',
1185+
installer: 'vite',
1186+
description: 'Vite is dev server that serves your source files over native ES modules',
1187+
minVersion: '>=2.0.0',
1188+
},
1189+
satisfied: false,
1190+
detectedVersion: '1.0.0',
1191+
loc: null,
1192+
},
1193+
],
1194+
],
1195+
}
1196+
},
11751197
})
11761198
})

packages/frontend-shared/src/warning/Warning.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
>
1111
<div
1212
ref="markdownTarget"
13+
class="warning-markdown"
1314
v-html="markdown"
1415
/>
1516
<Button
@@ -68,3 +69,14 @@ let message = computed(() => {
6869
6970
const { markdown } = useMarkdown(markdownTarget, message.value, { classes: { code: ['bg-warning-200'] } })
7071
</script>
72+
73+
<style lang="scss">
74+
// Add some extra margin to the <ul>
75+
// TODO: ideally move this into `frontend-shared/src/composables/useMarkdown`
76+
// It doesn't get applied when added there due to conflicting with other, higher priority rules.
77+
.warning-markdown {
78+
ul {
79+
@apply ml-16px mb-16px;
80+
}
81+
}
82+
</style>

packages/graphql/schemas/schema.graphql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ enum ErrorTypeEnum {
672672
CDP_VERSION_TOO_OLD
673673
CHROME_WEB_SECURITY_NOT_SUPPORTED
674674
COMPONENT_FOLDER_REMOVED
675+
COMPONENT_TESTING_MISMATCHED_DEPENDENCIES
675676
CONFIG_FILES_LANGUAGE_CONFLICT
676677
CONFIG_FILE_DEV_SERVER_INVALID_RETURN
677678
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID

packages/launchpad/cypress/e2e/config-warning.cy.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,55 @@ describe('experimentalStudio', () => {
111111
cy.get('[data-cy="warning-alert"]').contains('Warning: Experimental Studio Removed')
112112
})
113113
})
114+
115+
describe('component testing dependency warnings', () => {
116+
it('warns against outdated react and vite version', () => {
117+
cy.scaffoldProject('component-testing-outdated-dependencies')
118+
cy.addProject('component-testing-outdated-dependencies')
119+
cy.openGlobalMode()
120+
cy.visitLaunchpad()
121+
cy.contains('component-testing-outdated-dependencies').click()
122+
cy.get('[data-cy="warning-alert"]').should('not.exist')
123+
cy.get('a').contains('Projects').click()
124+
cy.get('[data-cy-testingtype="component"]').click()
125+
cy.get('[data-cy="warning-alert"]').should('exist')
126+
.should('contain.text', 'Warning: Component Testing Mismatched Dependencies')
127+
.should('contain.text', 'vite. Expected ^=2.0.0 || ^=3.0.0, found 2.0.0-beta.70')
128+
.should('contain.text', 'react. Expected ^=16.0.0 || ^=17.0.0 || ^=18.0.0, found 15.6.2.')
129+
.should('contain.text', 'react-dom. Expected ^=16.0.0 || ^=17.0.0 || ^=18.0.0 but dependency was not found.')
130+
131+
cy.get('.warning-markdown').find('li').should('have.length', 3)
132+
})
133+
134+
it('warns against outdated @vue/cli dependency', () => {
135+
cy.scaffoldProject('outdated-deps-vuecli3')
136+
cy.addProject('outdated-deps-vuecli3')
137+
cy.openGlobalMode()
138+
cy.visitLaunchpad()
139+
cy.contains('outdated-deps-vuecli3').click()
140+
cy.get('[data-cy="warning-alert"]').should('not.exist')
141+
cy.get('a').contains('Projects').click()
142+
cy.get('[data-cy-testingtype="component"]').click()
143+
cy.get('[data-cy="warning-alert"]').should('exist')
144+
.should('contain.text', 'Warning: Component Testing Mismatched Dependencies')
145+
.should('contain.text', '@vue/cli-service. Expected ^=4.0.0 || ^=5.0.0, found 3.12.1.')
146+
.should('contain.text', 'vue. Expected ^3.0.0, found 2.7.8.')
147+
148+
cy.get('.warning-markdown').find('li').should('have.length', 2)
149+
})
150+
151+
it('does not show warning for project with supported dependencies', () => {
152+
cy.scaffoldProject('vueclivue3-configured')
153+
cy.addProject('vueclivue3-configured')
154+
cy.openGlobalMode()
155+
cy.visitLaunchpad()
156+
cy.contains('vueclivue3-configured').click()
157+
cy.get('[data-cy="warning-alert"]').should('not.exist')
158+
cy.get('a').contains('Projects').click()
159+
cy.get('[data-cy-testingtype="component"]').click()
160+
161+
// Wait until launch browser screen and assert warning does not exist
162+
cy.contains('Choose a Browser')
163+
cy.get('[data-cy="warning-alert"]').should('not.exist')
164+
})
165+
})

packages/scaffold-config/src/dependencies.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export const WIZARD_DEPENDENCY_WEBPACK = {
44
package: 'webpack',
55
installer: 'webpack',
66
description: 'Webpack is a module bundler',
7-
minVersion: '>=4.0.0',
7+
minVersion: '>=4.0.0 || >=5.0.0',
88
} as const
99

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

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

4646
export const WIZARD_DEPENDENCY_TYPESCRIPT = {
@@ -58,7 +58,7 @@ export const WIZARD_DEPENDENCY_REACT_SCRIPTS = {
5858
package: 'react-scripts',
5959
installer: 'react-scripts',
6060
description: 'Create React apps with no build configuration',
61-
minVersion: '>=4.0.0',
61+
minVersion: '^=4.0.0 || ^=5.0.0',
6262
} as const
6363

6464
export const WIZARD_DEPENDENCY_VUE_CLI_SERVICE = {
@@ -67,7 +67,7 @@ export const WIZARD_DEPENDENCY_VUE_CLI_SERVICE = {
6767
package: '@vue/cli-service',
6868
installer: '@vue/cli-service',
6969
description: 'Standard Tooling for Vue.js Development',
70-
minVersion: '>=4.0.0',
70+
minVersion: '^=4.0.0 || ^=5.0.0',
7171
} as const
7272

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

8282
export const WIZARD_DEPENDENCY_NUXT = {
@@ -94,7 +94,7 @@ export const WIZARD_DEPENDENCY_NEXT = {
9494
package: 'next',
9595
installer: 'next',
9696
description: 'The React Framework for Production',
97-
minVersion: '>=10.0.0',
97+
minVersion: '^=10.0.0 || ^=11.0.0 || ^=12.0.0',
9898
} as const
9999

100100
export const WIZARD_DEPENDENCY_ANGULAR_CLI = {

packages/scaffold-config/src/frameworks.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ export function inPkgJson (dependency: WizardDependency, projectPath: string): D
2929
// TODO: convert to async FS method
3030
// eslint-disable-next-line no-restricted-syntax
3131
const pkg = fs.readJsonSync(loc) as PkgJson
32-
const pkgVersion = semver.coerce(pkg.version)
3332

34-
if (!pkgVersion) {
33+
if (!pkg.version) {
3534
throw Error(`${pkg.version} for ${dependency.package} is not a valid semantic version.`)
3635
}
3736

37+
const satisfied = Boolean(pkg.version && semver.satisfies(pkg.version, dependency.minVersion, {
38+
includePrerelease: true,
39+
}))
40+
3841
return {
3942
dependency,
4043
detectedVersion: pkg.version,
4144
loc,
42-
satisfied: Boolean(pkg.version && semver.satisfies(pkgVersion, dependency.minVersion)),
45+
satisfied,
4346
}
4447
} catch (e) {
4548
return {

0 commit comments

Comments
 (0)