chore: Check project dependencies for CT compatibility (#26497)

* chore: Check project dependencies for CT compatibility

* Cleanup
This commit is contained in:
Mike Plummer
2023-04-17 03:05:02 -05:00
committed by GitHub
parent c7da9f4ee6
commit 6209b91865
4 changed files with 128 additions and 11 deletions
@@ -1,5 +1,6 @@
import fetch from 'cross-fetch'
import type { DataContext } from '../DataContext'
import { isDependencyInstalled } from '@packages/scaffold-config'
// Require rather than import since data-context is stricter than network and there are a fair amount of errors in agent.
const { agent } = require('@packages/network')
@@ -18,4 +19,8 @@ export class UtilDataSource {
// which is what will be used here
return fetch(input, { agent, ...init })
}
isDependencyInstalled (dependency: Cypress.CypressComponentDependency, projectPath: string) {
return isDependencyInstalled(dependency, projectPath)
}
}
@@ -3,6 +3,8 @@ import type { DataContext } from '..'
import type { TestingType } from '@packages/types'
import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
import Debug from 'debug'
import { WIZARD_DEPENDENCIES } from '@packages/scaffold-config'
import semver from 'semver'
const debug = Debug('cypress:data-context:sources:VersionsDataSource')
@@ -160,6 +162,47 @@ export class VersionsDataSource {
}
}
try {
const projectPath = this.ctx.currentProject
if (projectPath) {
const dependenciesToCheck = WIZARD_DEPENDENCIES
debug('Checking %d dependencies in project', dependenciesToCheck.length)
// Check all dependencies of interest in parallel
const dependencyResults = await Promise.allSettled(
dependenciesToCheck.map(async (dependency) => {
const result = await this.ctx.util.isDependencyInstalled(dependency, projectPath)
// If a dependency isn't satisfied then we are no longer interested in it,
// exclude from further processing by rejecting promise
if (!result.satisfied) {
throw new Error('Unsatisfied dependency')
}
// We only want major version, fallback to `-1` if we couldn't detect version
const majorVersion = result.detectedVersion ? semver.major(result.detectedVersion) : -1
// For any satisfied dependencies, build a `package@version` string
return `${result.dependency.package}@${majorVersion}`
}),
)
// Take any dependencies that were found and combine into comma-separated string
const headerValue = dependencyResults
.filter(this.isFulfilled)
.map((result) => result.value)
.join(',')
if (headerValue) {
manifestHeaders['x-dependencies'] = headerValue
}
} else {
debug('No project path, skipping dependency check')
}
} catch (err) {
debug('Failed to detect project dependencies', err)
}
try {
const manifestResponse = await this.ctx.util.fetch(CYPRESS_REMOTE_MANIFEST_URL, {
headers: manifestHeaders,
@@ -190,4 +233,8 @@ export class VersionsDataSource {
return undefined
}
}
private isFulfilled<R> (item: PromiseSettledResult<R>): item is PromiseFulfilledResult<R> {
return item.status === 'fulfilled'
}
}
@@ -16,6 +16,7 @@ describe('VersionsDataSource', () => {
let ctx: DataContext
let nmiStub: sinon.SinonStub
let fetchStub: sinon.SinonStub
let isDependencyInstalledStub: sinon.SinonStub
let mockNow: Date = new Date()
let versionsDataSource: VersionsDataSource
let currentCypressVersion: string = pkg.version
@@ -23,14 +24,26 @@ describe('VersionsDataSource', () => {
before(() => {
ctx = createTestDataContext('open')
;(ctx.lifecycleManager as any)._cachedInitialConfig = {
component: {
devServer: {
framework: 'react',
bundler: 'vite',
},
},
}
ctx.coreData.currentProject = '/abc'
ctx.coreData.currentTestingType = 'e2e'
fetchStub = sinon.stub()
isDependencyInstalledStub = sinon.stub()
})
beforeEach(() => {
nmiStub = sinon.stub(nmi, 'machineId')
sinon.stub(ctx.util, 'fetch').callsFake(fetchStub)
sinon.stub(ctx.util, 'isDependencyInstalled').callsFake(isDependencyInstalledStub)
sinon.stub(os, 'platform').returns('darwin')
sinon.stub(os, 'arch').returns('x64')
sinon.useFakeTimers({ now: mockNow })
@@ -45,7 +58,7 @@ describe('VersionsDataSource', () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
@@ -54,7 +67,7 @@ describe('VersionsDataSource', () => {
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
},
}),
}).resolves({
json: sinon.stub().resolves({
name: 'Cypress',
@@ -99,7 +112,7 @@ describe('VersionsDataSource', () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
@@ -107,7 +120,7 @@ describe('VersionsDataSource', () => {
'x-initial-launch': String(false),
'x-testing-type': 'component',
'x-logged-in': 'false',
},
}),
}).resolves({
json: sinon.stub().resolves({
name: 'Cypress',
@@ -121,7 +134,7 @@ describe('VersionsDataSource', () => {
versionsDataSource.resetLatestVersionTelemetry()
const latestVersion = await ctx.coreData.versionData.latestVersion
const latestVersion = await ctx.coreData.versionData?.latestVersion
expect(latestVersion).to.eql('16.0.0')
})
@@ -131,7 +144,7 @@ describe('VersionsDataSource', () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
@@ -140,7 +153,7 @@ describe('VersionsDataSource', () => {
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
},
}),
})
.rejects()
.withArgs(NPM_CYPRESS_REGISTRY_URL)
@@ -158,7 +171,7 @@ describe('VersionsDataSource', () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
@@ -167,7 +180,7 @@ describe('VersionsDataSource', () => {
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
},
}),
})
.callsFake(async () => new Response('Error'))
.withArgs(NPM_CYPRESS_REGISTRY_URL)
@@ -183,9 +196,60 @@ describe('VersionsDataSource', () => {
versionsDataSource.resetLatestVersionTelemetry()
await ctx.coreData.versionData.latestVersion
await ctx.coreData.versionData?.latestVersion
expect(versionInfo.current.version).to.eql(currentCypressVersion)
})
it('generates x-framework, x-bundler, and x-dependencies headers', async () => {
isDependencyInstalledStub.callsFake(async (dependency) => {
// Should include any resolved dependency with a valid version
if (dependency.package === 'react') {
return {
dependency,
detectedVersion: '1.2.3',
satisfied: true,
} as Cypress.DependencyToInstall
}
// Not satisfied dependency should be excluded
if (dependency.package === 'vue') {
return {
dependency,
detectedVersion: '4.5.6',
satisfied: false,
}
}
// Satisfied dependency without resolved version should result in -1
if (dependency.package === 'typescript') {
return {
dependency,
detectedVersion: null,
satisfied: true,
}
}
// Any dependencies that error while resolving should be excluded
throw new Error('Failed check')
})
ctx.coreData.currentTestingType = 'component'
versionsDataSource = new VersionsDataSource(ctx)
ctx.coreData.currentTestingType = 'e2e'
versionsDataSource.resetLatestVersionTelemetry()
await versionsDataSource.versionData()
expect(fetchStub).to.have.been.calledWith(
CYPRESS_REMOTE_MANIFEST_URL,
{
headers: sinon.match({
'x-framework': 'react',
'x-dev-server': 'vite',
'x-dependencies': 'typescript@-1,react@1',
}),
},
)
})
})
})
@@ -65,7 +65,7 @@ describe('Launchpad: Open Mode', () => {
cy.openProject('todos', ['--e2e'])
})
it('includes x-framework and x-dev-server, even when launched in e2e mode', () => {
it('includes `x-framework`, `x-dev-server`, and `x-dependencies` headers, even when launched in e2e mode', () => {
cy.visitLaunchpad()
cy.skipWelcome()
cy.get('h1').should('contain', 'Choose a browser')
@@ -74,6 +74,7 @@ describe('Launchpad: Open Mode', () => {
headers: {
'x-framework': 'react',
'x-dev-server': 'webpack',
'x-dependencies': 'typescript@4',
},
})
})