mirror of
https://github.com/cypress-io/cypress.git
synced 2026-02-05 14:50:00 -06:00
chore: record event gql mutation (#27416)
* feat: record event gql mutation * Allow projectSlug to be optional * Do not start source in run mode * Fix unit test * ts linting fix * Adding test * Adding test * refactor --------- Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
This commit is contained in:
@@ -115,6 +115,10 @@ export class DataContext {
|
||||
return this._config.mode === 'run'
|
||||
}
|
||||
|
||||
get isOpenMode () {
|
||||
return !this.isRunMode
|
||||
}
|
||||
|
||||
@cached
|
||||
get graphql () {
|
||||
return new GraphQLDataSource()
|
||||
@@ -236,6 +240,9 @@ export class DataContext {
|
||||
getUser: () => this.user,
|
||||
logout: () => this.actions.auth.logout().catch(this.logTraceError),
|
||||
invalidateClientUrqlCache: () => this.graphql.invalidateClientUrqlCache(this),
|
||||
headers: {
|
||||
getMachineId: this.coreData.machineId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -294,4 +294,12 @@ export class DataEmitterActions extends DataEmitterEvents {
|
||||
|
||||
return iterator
|
||||
}
|
||||
|
||||
subscribeToRawEvent (evt: keyof DataEmitterEvents, listener: Parameters<EventEmitter['on']>[1]) {
|
||||
this.pub.on(evt, listener)
|
||||
}
|
||||
|
||||
unsubscribeToRawEvent (evt: keyof DataEmitterEvents, listener: Parameters<EventEmitter['on']>[1]) {
|
||||
this.pub.off(evt, listener)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { gql } from '@urql/core'
|
||||
import type { DataContext } from '..'
|
||||
import Debug from 'debug'
|
||||
import type { LocalTestCountsInput } from '@packages/graphql/src/gen/nxs.gen'
|
||||
|
||||
const pkg = require('@packages/root')
|
||||
|
||||
@@ -14,6 +16,10 @@ interface CollectibleEvent {
|
||||
machineId?: string
|
||||
}
|
||||
|
||||
type EventInputs = {
|
||||
localTestCounts?: LocalTestCountsInput
|
||||
}
|
||||
|
||||
/**
|
||||
* Defaults to staging when doing development. To override to production for development,
|
||||
* explicitly set process.env.CYPRESS_INTERNAL_ENV to 'production`
|
||||
@@ -56,4 +62,21 @@ export class EventCollectorActions {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
recordEventGQL (eventInputs: EventInputs) {
|
||||
const RECORD_EVENT_GQL = gql`
|
||||
mutation EventCollectorActions_RecordEvent($localTestCounts: LocalTestCountsInput) {
|
||||
cloudRecordEvent(localTestCounts: $localTestCounts)
|
||||
}
|
||||
`
|
||||
|
||||
debug('recordEventGQL final variables %o', eventInputs)
|
||||
|
||||
return this.ctx.cloud.executeRemoteGraphQL({
|
||||
operationType: 'mutation',
|
||||
fieldName: 'cloudRecordEvent',
|
||||
operationDoc: RECORD_EVENT_GQL,
|
||||
operationVariables: eventInputs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,3 +181,16 @@ export async function hasNonExampleSpec (testTemplateDir: string, specs: string[
|
||||
|
||||
return specs.some((spec) => !specInTemplates(spec))
|
||||
}
|
||||
|
||||
export async function getExampleSpecPaths (testTemplateDir: string): Promise<string[]> {
|
||||
debug(`getExampleSpecPaths - calling with template directory "${testTemplateDir}"`)
|
||||
const dirExists = await fileExists(testTemplateDir)
|
||||
|
||||
if (!dirExists) {
|
||||
throw new Error(`Template directory does not exist: ${testTemplateDir}`)
|
||||
}
|
||||
|
||||
const templateFiles = await allFilesInDir(testTemplateDir)
|
||||
|
||||
return templateFiles.map((templateFile) => templateFile.substring(testTemplateDir.length + 1))
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import type { DataContext } from '..'
|
||||
import assert from 'assert'
|
||||
import type { AllModeOptions, FoundBrowser, FullConfig, TestingType } from '@packages/types'
|
||||
import { autoBindDebug } from '../util/autoBindDebug'
|
||||
import { GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
import { EventCollectorSource, GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
import { OnFinalConfigLoadedOptions, ProjectConfigManager } from './ProjectConfigManager'
|
||||
import pDefer from 'p-defer'
|
||||
import { EventRegistrar } from './EventRegistrar'
|
||||
@@ -433,6 +433,11 @@ export class ProjectLifecycleManager {
|
||||
},
|
||||
})
|
||||
|
||||
s.eventCollectorSource?.destroy()
|
||||
if (this.ctx.isOpenMode) {
|
||||
s.eventCollectorSource = new EventCollectorSource(this.ctx)
|
||||
}
|
||||
|
||||
s.diagnostics = { error: null, warnings: [] }
|
||||
s.packageManager = packageManagerUsed
|
||||
})
|
||||
@@ -574,6 +579,7 @@ export class ProjectLifecycleManager {
|
||||
|
||||
await this.ctx.coreData.currentProjectGitInfo?.destroy()
|
||||
await this.ctx.project.destroy()
|
||||
await this.ctx.coreData.eventCollectorSource?.destroy()
|
||||
this._currentTestingType = null
|
||||
this._cachedInitialConfig = undefined
|
||||
this._cachedFullConfig = undefined
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ChildProcess } from 'child_process'
|
||||
import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
|
||||
import type { Server } from 'http'
|
||||
import type { ErrorWrapperSource } from '@packages/errors'
|
||||
import type { GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
import type { EventCollectorSource, GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
import { machineId as getMachineId } from 'node-machine-id'
|
||||
|
||||
export type Maybe<T> = T | null | undefined
|
||||
@@ -163,6 +163,7 @@ export interface CoreDataShape {
|
||||
npmMetadata: Promise<Record<string, string>>
|
||||
} | null
|
||||
cloudProject: CloudDataShape
|
||||
eventCollectorSource: EventCollectorSource | null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,6 +241,7 @@ export function makeCoreData (modeOptions: Partial<AllModeOptions> = {}): CoreDa
|
||||
cloudProject: {
|
||||
testsForRunResults: {},
|
||||
},
|
||||
eventCollectorSource: null,
|
||||
}
|
||||
|
||||
async function machineId (): Promise<string | null> {
|
||||
|
||||
@@ -73,6 +73,9 @@ export interface CloudDataSourceParams {
|
||||
* and we need to clear both the server & client side cache
|
||||
*/
|
||||
invalidateClientUrqlCache(): void
|
||||
headers?: {
|
||||
getMachineId: Promise<string | null>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,10 +102,11 @@ export class CloudDataSource {
|
||||
return this.params.getUser()
|
||||
}
|
||||
|
||||
get #additionalHeaders () {
|
||||
async #additionalHeaders () {
|
||||
return {
|
||||
'Authorization': this.#user ? `bearer ${this.#user.authToken}` : '',
|
||||
'x-cypress-version': pkg.version,
|
||||
'x-machine-id': await this.params.headers?.getMachineId || '',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +160,7 @@ export class CloudDataSource {
|
||||
...init,
|
||||
headers: {
|
||||
...init?.headers,
|
||||
...this.#additionalHeaders,
|
||||
...await this.#additionalHeaders(),
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
78
packages/data-context/src/sources/EventCollectorSource.ts
Normal file
78
packages/data-context/src/sources/EventCollectorSource.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import Debug from 'debug'
|
||||
import dayjs from 'dayjs'
|
||||
import type { DataContext } from '..'
|
||||
import type { CloudTestingTypeEnum, LocalTestCountsInput } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import { getTestCounts } from '../util/testCounts'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const debug = Debug('cypress:data-context:sources:EventCollectorSource')
|
||||
|
||||
export class EventCollectorSource {
|
||||
constructor (private ctx: DataContext) {
|
||||
debug('Starting')
|
||||
ctx.emitter.subscribeToRawEvent('authChange', this.#localTestCountsListener)
|
||||
ctx.emitter.subscribeToRawEvent('configChange', this.#localTestCountsListener)
|
||||
}
|
||||
|
||||
#localTestCountsListener = debounce(() => {
|
||||
this.sendLocalTestCounts()
|
||||
.catch((error) => {
|
||||
debug('error caught from sending counts', error)
|
||||
})
|
||||
}, 250)
|
||||
|
||||
destroy () {
|
||||
this.ctx.emitter.unsubscribeToRawEvent('authChange', this.#localTestCountsListener)
|
||||
this.ctx.emitter.unsubscribeToRawEvent('configChange', this.#localTestCountsListener)
|
||||
}
|
||||
|
||||
async sendLocalTestCounts () {
|
||||
debug('Checking to send local test counts')
|
||||
if (!this.ctx.coreData.currentTestingType) {
|
||||
debug('will not send - no testing type')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const user = this.ctx.coreData.user
|
||||
const isAuthenticated = !!user && !!user.name
|
||||
|
||||
if (!isAuthenticated) {
|
||||
debug('will not send - not authenticated')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const currentLocalPreferences = this.ctx.project.getCurrentProjectSavedState()
|
||||
const lastTestCountsEvent = currentLocalPreferences?.lastTestCountsEvent
|
||||
const thirtyDaysAgo = dayjs().subtract(30, 'days')
|
||||
const hasBeenSentLast30Days = !!lastTestCountsEvent && thirtyDaysAgo.isBefore(dayjs(lastTestCountsEvent))
|
||||
|
||||
if (hasBeenSentLast30Days) {
|
||||
debug('will not send', { isAuthenticated, hasBeenSentLast30Days })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const testingType: CloudTestingTypeEnum = this.ctx.coreData.currentTestingType === 'e2e' ? 'E2E' : 'COMPONENT'
|
||||
const testCounts = await getTestCounts(this.ctx.project.specs)
|
||||
const projectSlug = await this.ctx.project.projectId()
|
||||
|
||||
const localTestCounts: LocalTestCountsInput = {
|
||||
projectSlug,
|
||||
testingType,
|
||||
...testCounts,
|
||||
branch: this.ctx.git?.currentBranch,
|
||||
}
|
||||
|
||||
debug('sending recordEvent for local test counts', localTestCounts)
|
||||
|
||||
const result = await this.ctx.actions.eventCollector.recordEventGQL({ localTestCounts })
|
||||
|
||||
if (result.data?.cloudRecordEvent === true) {
|
||||
await this.ctx.actions.localSettings.setPreferences(JSON.stringify({ lastTestCountsEvent: Date.now() }), 'project')
|
||||
}
|
||||
|
||||
debug('result', result)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export * from './BrowserDataSource'
|
||||
export * from './CloudDataSource'
|
||||
export * from './EnvDataSource'
|
||||
export * from './ErrorDataSource'
|
||||
export * from './EventCollectorSource'
|
||||
export * from './FileDataSource'
|
||||
export * from './GitDataSource'
|
||||
export * from './GraphQLDataSource'
|
||||
|
||||
@@ -8,5 +8,6 @@ export * from './config-file-updater'
|
||||
export * from './file'
|
||||
export * from './hasTypescript'
|
||||
export * from './pluginHandlers'
|
||||
export * from './testCounts'
|
||||
export * from './urqlCacheKeys'
|
||||
export * from './weightedChoice'
|
||||
|
||||
66
packages/data-context/src/util/testCounts.ts
Normal file
66
packages/data-context/src/util/testCounts.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import fs from 'fs'
|
||||
import readline from 'readline'
|
||||
import Debug from 'debug'
|
||||
import type { SpecWithRelativeRoot } from '@packages/types'
|
||||
import { getExampleSpecPaths } from '../codegen'
|
||||
import templates from '../codegen/templates'
|
||||
|
||||
const debug = Debug('cypress:data-context:util:testCounts')
|
||||
|
||||
export async function getTestCounts (specs: SpecWithRelativeRoot[]) {
|
||||
const templateSpecPaths = await getExampleSpecPaths(templates.e2eExamples)
|
||||
|
||||
const startTime = performance.now()
|
||||
|
||||
const specCountPromises = specs.map((spec) => {
|
||||
return new Promise<{path: string, isExample: boolean, testCounts: number}>((resolve, reject) => {
|
||||
let testCounts = 0
|
||||
|
||||
readline.createInterface({
|
||||
input: fs.createReadStream(spec.absolute),
|
||||
})
|
||||
.on('line', (line) => {
|
||||
// test for "it(" appearing at beginning of line or with space before
|
||||
const isTest = /(^| )it\(/.test(line)
|
||||
|
||||
if (isTest) {
|
||||
testCounts++
|
||||
}
|
||||
})
|
||||
.on('close', () => {
|
||||
resolve({
|
||||
path: spec.absolute,
|
||||
isExample: templateSpecPaths.some((templateSpec) => templateSpec === spec.relativeToCommonRoot),
|
||||
testCounts,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const countResults = await Promise.all(specCountPromises)
|
||||
|
||||
interface CountSummary {
|
||||
totalTests: number
|
||||
exampleSpecs: number
|
||||
exampleTests: number
|
||||
}
|
||||
|
||||
const countSummary = countResults.reduce<CountSummary>((summary, curr) => {
|
||||
summary.totalTests += curr.testCounts
|
||||
if (curr.isExample) {
|
||||
summary.exampleSpecs++
|
||||
summary.exampleTests += curr.testCounts
|
||||
}
|
||||
|
||||
return summary
|
||||
}, { totalTests: 0, exampleSpecs: 0, exampleTests: 0 })
|
||||
|
||||
const totalTime = performance.now() - startTime
|
||||
|
||||
debug(`took ${totalTime} ms to count ${specs.length} specs`)
|
||||
|
||||
return {
|
||||
totalSpecs: specs.length,
|
||||
...countSummary,
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,12 @@ import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { DataContext } from '../../../src'
|
||||
import {
|
||||
Action, codeGenerator, CodeGenResult, CodeGenResults, hasNonExampleSpec,
|
||||
Action,
|
||||
codeGenerator,
|
||||
CodeGenResult,
|
||||
CodeGenResults,
|
||||
hasNonExampleSpec,
|
||||
getExampleSpecPaths,
|
||||
} from '../../../src/codegen/code-generator'
|
||||
import { SpecOptions } from '../../../src/codegen/spec-options'
|
||||
import templates from '../../../src/codegen/templates'
|
||||
@@ -415,4 +420,22 @@ describe('code-generator', () => {
|
||||
expect(async () => await hasNonExampleSpec('', singleSpec)).to.throw
|
||||
})
|
||||
})
|
||||
|
||||
context('hasNonExampleSpec', async () => {
|
||||
it('should error if template dir does not exist', () => {
|
||||
expect(async () => await getExampleSpecPaths('')).to.throw
|
||||
})
|
||||
|
||||
it('should return relative paths to example specs', async () => {
|
||||
const results = await getExampleSpecPaths(templates.e2eExamples)
|
||||
|
||||
expect(results.length).to.be.greaterThan(0)
|
||||
|
||||
results.forEach((specPath) => {
|
||||
const fullPathToSpec = path.join(templates.e2eExamples, specPath)
|
||||
|
||||
expect(fs.pathExistsSync(fullPathToSpec), `expected to find file at ${fullPathToSpec}`).to.be.true
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import sinon from 'sinon'
|
||||
import { execute, print } from 'graphql'
|
||||
import { execute } from 'graphql'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { Response } from 'cross-fetch'
|
||||
|
||||
@@ -56,7 +56,6 @@ describe('CloudDataSource', () => {
|
||||
getUserStub.returns(null)
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -69,7 +68,6 @@ describe('CloudDataSource', () => {
|
||||
it('issues a fetch request for the data when the user is defined', async () => {
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -83,14 +81,12 @@ describe('CloudDataSource', () => {
|
||||
it('only issues a single fetch if the operation is called twice', async () => {
|
||||
const result1 = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
})
|
||||
const result2 = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -107,7 +103,6 @@ describe('CloudDataSource', () => {
|
||||
it('resolves eagerly with the cached data if the data has already been resolved', async () => {
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -117,7 +112,6 @@ describe('CloudDataSource', () => {
|
||||
|
||||
const immediateResult = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -130,7 +124,6 @@ describe('CloudDataSource', () => {
|
||||
it('when there is a nullable field missing, resolves with the eager result & fetches for the rest', async () => {
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -142,7 +135,6 @@ describe('CloudDataSource', () => {
|
||||
|
||||
const immediateResult = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_WITH_OPTIONAL_MISSING),
|
||||
operationDoc: FAKE_USER_WITH_OPTIONAL_MISSING,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -161,7 +153,6 @@ describe('CloudDataSource', () => {
|
||||
it('when there is a non-nullable field missing, issues the remote query immediately', async () => {
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -173,7 +164,6 @@ describe('CloudDataSource', () => {
|
||||
|
||||
const requiredResult = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_WITH_REQUIRED_MISSING),
|
||||
operationDoc: FAKE_USER_WITH_REQUIRED_MISSING,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -191,7 +181,6 @@ describe('CloudDataSource', () => {
|
||||
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -209,7 +198,6 @@ describe('CloudDataSource', () => {
|
||||
|
||||
const result = cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
@@ -228,7 +216,6 @@ describe('CloudDataSource', () => {
|
||||
describe('isResolving', () => {
|
||||
it('returns false if we are not currently resolving the request', () => {
|
||||
const result = cloudDataSource.isResolving({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})
|
||||
@@ -239,14 +226,12 @@ describe('CloudDataSource', () => {
|
||||
it('returns true if we are currently resolving the request', () => {
|
||||
cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
})
|
||||
|
||||
const result = cloudDataSource.isResolving({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})
|
||||
@@ -258,7 +243,6 @@ describe('CloudDataSource', () => {
|
||||
describe('hasResolved', () => {
|
||||
it('returns false if we have not resolved the data yet', () => {
|
||||
const result = cloudDataSource.hasResolved({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})
|
||||
@@ -269,14 +253,12 @@ describe('CloudDataSource', () => {
|
||||
it('returns true if we have resolved the data for the query', async () => {
|
||||
await cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
})
|
||||
|
||||
const result = cloudDataSource.hasResolved({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})
|
||||
@@ -289,14 +271,12 @@ describe('CloudDataSource', () => {
|
||||
it('allows us to issue a cache.invalidate on individual fields in the cloud schema', async () => {
|
||||
await cloudDataSource.executeRemoteGraphQL({
|
||||
fieldName: 'cloudViewer',
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
operationType: 'query',
|
||||
})
|
||||
|
||||
expect(cloudDataSource.hasResolved({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})).to.eq(true)
|
||||
@@ -304,7 +284,6 @@ describe('CloudDataSource', () => {
|
||||
await cloudDataSource.invalidate('Query', 'cloudViewer')
|
||||
|
||||
expect(cloudDataSource.hasResolved({
|
||||
operation: print(FAKE_USER_QUERY),
|
||||
operationDoc: FAKE_USER_QUERY,
|
||||
operationVariables: {},
|
||||
})).to.eq(false)
|
||||
|
||||
54
packages/data-context/test/unit/util/testCounts.spec.ts
Normal file
54
packages/data-context/test/unit/util/testCounts.spec.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { SpecWithRelativeRoot } from '@packages/types'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import fs from 'fs-extra'
|
||||
import { scaffoldMigrationProject } from '../helper'
|
||||
import { getTestCounts } from '../../../src/util/testCounts'
|
||||
import path from 'path'
|
||||
|
||||
describe('getTestCounts', () => {
|
||||
it('should return zeros for no input', async () => {
|
||||
const specs = []
|
||||
|
||||
const counts = await getTestCounts(specs)
|
||||
|
||||
expect(counts).to.deep.equal({
|
||||
totalSpecs: 0,
|
||||
totalTests: 0,
|
||||
exampleSpecs: 0,
|
||||
exampleTests: 0,
|
||||
})
|
||||
})
|
||||
|
||||
context('with e2e project', () => {
|
||||
let specs: SpecWithRelativeRoot[]
|
||||
|
||||
beforeEach(async () => {
|
||||
const cwd = await scaffoldMigrationProject('e2e')
|
||||
|
||||
const e2eSpecs = await fs.readdir(path.join(cwd, 'cypress/e2e'))
|
||||
|
||||
specs = e2eSpecs.map((spec) => {
|
||||
const absolute = path.join(cwd, 'cypress/e2e', spec)
|
||||
|
||||
return {
|
||||
absolute,
|
||||
relativeToCommonRoot: path.join('cypress/e2e', spec),
|
||||
} as SpecWithRelativeRoot
|
||||
})
|
||||
.filter((spec) => {
|
||||
return !fs.lstatSync(spec.absolute).isDirectory()
|
||||
})
|
||||
})
|
||||
|
||||
it('should return counts for tests e2e migration project', async () => {
|
||||
const counts = await getTestCounts(specs)
|
||||
|
||||
expect(counts.totalSpecs).to.equal(specs.length)
|
||||
// don't test for exact number since tests in sample project might change
|
||||
expect(counts.totalTests).to.be.greaterThan(0)
|
||||
expect(counts.exampleSpecs).to.eq(0)
|
||||
expect(counts.exampleTests).to.eq(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -810,6 +810,11 @@ type CloudRunInstance implements Node {
|
||||
"""
|
||||
groupId: String!
|
||||
|
||||
"""
|
||||
Used to determine if the instance has test replay
|
||||
"""
|
||||
hasReplay: Boolean
|
||||
|
||||
"""
|
||||
Used to determine if the instance has screenshots
|
||||
"""
|
||||
@@ -826,6 +831,11 @@ type CloudRunInstance implements Node {
|
||||
hasVideo: Boolean!
|
||||
id: ID!
|
||||
|
||||
"""
|
||||
Link to Cypress Cloud to view test replay, if available
|
||||
"""
|
||||
replayUrl: String
|
||||
|
||||
"""
|
||||
Link to Cypress Cloud to view stdout, if available
|
||||
"""
|
||||
@@ -1094,6 +1104,14 @@ enum CloudTestResultStateEnum {
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
"""
|
||||
Type of tests
|
||||
"""
|
||||
enum CloudTestingTypeEnum {
|
||||
COMPONENT
|
||||
E2E
|
||||
}
|
||||
|
||||
"""
|
||||
A CloudUser represents an User stored in the Cypress Cloud
|
||||
"""
|
||||
@@ -1175,6 +1193,46 @@ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `dat
|
||||
"""
|
||||
scalar DateTime
|
||||
|
||||
"""
|
||||
Counts for specs and tests from a local project at a point in time
|
||||
"""
|
||||
input LocalTestCountsInput {
|
||||
"""
|
||||
Current Git branch name for local project
|
||||
"""
|
||||
branch: String
|
||||
|
||||
"""
|
||||
Total number of example specs found in project
|
||||
"""
|
||||
exampleSpecs: Int!
|
||||
|
||||
"""
|
||||
Total number of tests found in example specs. This can be an estimate
|
||||
"""
|
||||
exampleTests: Int!
|
||||
|
||||
"""
|
||||
Project slug for project
|
||||
"""
|
||||
projectSlug: String
|
||||
|
||||
"""
|
||||
Testing type
|
||||
"""
|
||||
testingType: CloudTestingTypeEnum!
|
||||
|
||||
"""
|
||||
Total number of specs found in project
|
||||
"""
|
||||
totalSpecs: Int!
|
||||
|
||||
"""
|
||||
Total number of tests found in all specs in project. This can be an estimate
|
||||
"""
|
||||
totalTests: Int!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
Create a project in the dashboard and return its object
|
||||
@@ -1195,6 +1253,11 @@ type Mutation {
|
||||
"""
|
||||
cloudProjectRequestAccess(projectSlug: String!): CloudProjectResult
|
||||
|
||||
"""
|
||||
Record event
|
||||
"""
|
||||
cloudRecordEvent(localTestCounts: LocalTestCountsInput): Boolean
|
||||
|
||||
"""
|
||||
Adding as a test
|
||||
"""
|
||||
|
||||
@@ -646,6 +646,9 @@ type CloudRunInstance implements Node {
|
||||
"""The ID of the group that this instance belongs to"""
|
||||
groupId: String!
|
||||
|
||||
"""Used to determine if the instance has test replay"""
|
||||
hasReplay: Boolean
|
||||
|
||||
"""Used to determine if the instance has screenshots"""
|
||||
hasScreenshots: Boolean!
|
||||
|
||||
@@ -656,6 +659,9 @@ type CloudRunInstance implements Node {
|
||||
hasVideo: Boolean!
|
||||
id: ID!
|
||||
|
||||
"""Link to Cypress Cloud to view test replay, if available"""
|
||||
replayUrl: String
|
||||
|
||||
"""Link to Cypress Cloud to view stdout, if available"""
|
||||
screenshotsUrl: String
|
||||
|
||||
@@ -842,6 +848,12 @@ enum CloudTestResultStateEnum {
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
"""Type of tests"""
|
||||
enum CloudTestingTypeEnum {
|
||||
COMPONENT
|
||||
E2E
|
||||
}
|
||||
|
||||
"""A CloudUser represents an User stored in the Cypress Cloud"""
|
||||
type CloudUser implements Node {
|
||||
"""Url to manage cloud organizations for this user"""
|
||||
@@ -1426,6 +1438,32 @@ type LocalSettingsPreferences {
|
||||
wasBrowserSetInCLI: Boolean
|
||||
}
|
||||
|
||||
"""Counts for specs and tests from a local project at a point in time"""
|
||||
input LocalTestCountsInput {
|
||||
"""Current Git branch name for local project"""
|
||||
branch: String
|
||||
|
||||
"""Total number of example specs found in project"""
|
||||
exampleSpecs: Int!
|
||||
|
||||
"""Total number of tests found in example specs. This can be an estimate"""
|
||||
exampleTests: Int!
|
||||
|
||||
"""Project slug for project"""
|
||||
projectSlug: String
|
||||
|
||||
"""Testing type"""
|
||||
testingType: CloudTestingTypeEnum!
|
||||
|
||||
"""Total number of specs found in project"""
|
||||
totalSpecs: Int!
|
||||
|
||||
"""
|
||||
Total number of tests found in all specs in project. This can be an estimate
|
||||
"""
|
||||
totalTests: Int!
|
||||
}
|
||||
|
||||
type ManualMigration implements Node {
|
||||
"""is the manual migration completed (all files are moved)"""
|
||||
completed: Boolean!
|
||||
@@ -1598,6 +1636,9 @@ type Mutation {
|
||||
|
||||
"""Request access to an organization from a projectId"""
|
||||
cloudProjectRequestAccess(projectSlug: String!): CloudProjectResult
|
||||
|
||||
"""Record event"""
|
||||
cloudRecordEvent(localTestCounts: LocalTestCountsInput): Boolean
|
||||
completeSetup: Query
|
||||
|
||||
"""add the passed text to the local clipboard"""
|
||||
|
||||
@@ -37,6 +37,7 @@ export const allowedKeys: Readonly<Array<keyof AllowedState>> = [
|
||||
'firstOpened',
|
||||
'lastOpened',
|
||||
'lastProjectId',
|
||||
'lastTestCountsEvent',
|
||||
'promptsShown',
|
||||
'specFilter',
|
||||
'preferredEditorBinary',
|
||||
@@ -76,6 +77,7 @@ export type AllowedState = Partial<{
|
||||
lastProjectId: Maybe<string>
|
||||
firstOpened: Maybe<number>
|
||||
lastOpened: Maybe<number>
|
||||
lastTestCountsEvent: Maybe<number>
|
||||
promptsShown: Maybe<object>
|
||||
specFilter: Maybe<string>
|
||||
preferredEditorBinary: Maybe<string>
|
||||
|
||||
Reference in New Issue
Block a user