chore: convert @packages/data-context from mocha to jest (#32598)

* start jest conversion and covnert authaction to jest

* chore: conver codegenaction to jest

* chore: convert cohorts action spec to vitest

* chore: convert dataEmitterActions to jest

* chore: convert event collector action to vitest

* chore: convert local settings action spec tojest

* chore: convert notification action to vitest

* chor: convert project actions to vitest and fix pretty-formnat

* chore: convert code-generator spec to jest

* chore: convert spec options spec to jest

* chore: convert project config ipc to jest

* chore: convert projectconfigmanager tests to jest

* chore: convert project lifecycle manager tests to jest

* chore: conver poller spec to jest

* chore: convert browser data source to jest

* chore: convert cloud dats source to jest

* chore: convert gitdatasource unit tests to jest

* chore: convert FileDataSource to jest

* chore: convert git data source to jest

* chore: convert graphql data source to jest

* chore: convert project data source to jest

* chore: convert revelvant runs data source to jest

* chore: convert relevant run specs data source to jest

* chore: convert remote request data source to jest

* chore: convert versions data source spec to jest

* chore: convert wizard data source spec to jest

* chore: convert document node builder to jest

* chore: convert has TypeScript to jest

* chore: convert test counts spec to jest

* chore: convert weighted choice spec to jest

* chore: convert config file updater to jest

* clean up files to allow tests to run in band but out of order, add README, cleanup unused scripts and dependencies

* fix deprecation warning from ts-jest via isolated modules

* chore: adjust expected result count

* chore: clean up types

* turn back on no implicit any and reinstall graphql package in frontend shared

* fix isFocusSupported mock

* chore: copy over the old data-context-helper into the server as the one in the data-context package is written in jest now

* chore: fix code review findings from cursor

* fix incorrect expect async throw

* chore: remove use of this in projectData source

* add docs to copied data-context helper and fix cursor detected issue
This commit is contained in:
Bill Glesias
2025-09-30 17:16:17 -04:00
committed by GitHub
parent f9c3f75563
commit 504b153005
45 changed files with 2657 additions and 1336 deletions
+1 -1
View File
@@ -1784,7 +1784,7 @@ jobs:
source ./scripts/ensure-node.sh
yarn lerna run types
- sanitize-verify-and-store-mocha-results:
expectedResultCount: 14
expectedResultCount: 13
verify-release-readiness:
<<: *defaults
+1 -1
View File
@@ -3486,7 +3486,7 @@ jobs:
yarn lerna run types
name: Test types
- sanitize-verify-and-store-mocha-results:
expectedResultCount: 14
expectedResultCount: 13
working_directory: ~/cypress
v8-integration-tests:
environment:
+1 -1
View File
@@ -87,7 +87,7 @@
##### Binary Packages
- [x] packages/config ✅ **COMPLETED**
- [ ] packages/data-context
- [x] packages/data-context **COMPLETED** (migrated from `mocha`/`sinon`/`chai` to `jest`). See package README for more details as to why `jest` over `vitest`
- [x] packages/driver ✅ **COMPLETED**
- [x] packages/electron ✅ **COMPLETED**
- [x] packages/error ✅ **COMPLETED**
-1
View File
@@ -274,7 +274,6 @@
"**/@types/cheerio": "0.22.21",
"**/@types/enzyme": "3.10.5",
"**/jquery": "3.7.1",
"**/pretty-format": "26.4.0",
"**/socket.io-parser": "4.0.5",
"**/ua-parser-js": "0.7.33",
"@definitelytyped/typescript-versions": "0.1.7",
+1
View File
@@ -0,0 +1 @@
foo-project/src/components/App.jsx
+7 -1
View File
@@ -157,4 +157,10 @@ export const Query = objectType({
})
}
})
```
```
### Testing
This package uses [jest](https://jestjs.io/) to integration/unit test this package. [vitest](https://vitest.dev/) currently cannot be used due to how `graphql` in this package is loaded into the vitest service worker, which is likely due to multiple instances of the GraphQL server being available. Since `jest` runs in a shared process, we are currently leveraging `jest` in place of `vitest`.
Additionally, Because the [ProjectLifecycleManager](https://github.com/cypress-io/cypress/blob/v15.3.0/packages/data-context/src/data/ProjectLifecycleManager.ts#L436) changes the current working directory for the process, we cannot run the tests in parallel, hence why the `--runInBand` option is utilized for running the tests.
+17
View File
@@ -0,0 +1,17 @@
import type { Config } from 'jest'
// @see https://kulshekhar.github.io/ts-jest/docs for documentation on ts-jest
import { createDefaultPreset } from 'ts-jest'
const tsJestTransformCfg = createDefaultPreset({
tsconfig: 'tsconfig.test.json',
}).transform
export default async (): Promise<Config> => {
return {
testMatch: ['<rootDir>/test/**/*.spec.ts'],
testEnvironment: 'node',
transform: {
...tsJestTransformCfg,
},
}
}
+5 -11
View File
@@ -15,7 +15,8 @@
"lint": "eslint --ext .js,.ts,.json, .",
"nexus-build": "ts-node ./scripts/nexus-build.ts",
"test": "yarn test-unit",
"test-unit": "mocha -r @packages/ts/register --config ./test/.mocharc.js --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json",
"test-debug": "node --experimental-vm-modules --inspect-brk node_modules/.bin/jest --runInBand --testTimeout=600000",
"test-unit": "yarn node --experimental-vm-modules node_modules/.bin/jest --runInBand",
"tslint": "tslint --config ../ts/tslint.json --project . --exclude ./src/gen/nxs.gen.ts"
},
"dependencies": {
@@ -101,24 +102,17 @@
"@types/ejs": "^3.1.2",
"@types/fs-extra": "^8.0.1",
"@types/graphql-resolve-batch": "1.1.6",
"@types/jest": "^30.0.0",
"@types/micromatch": "4.0.9",
"@types/mocha": "^8.0.3",
"@types/parse-glob": "3.0.29",
"@types/prettier": "2.4.3",
"@types/server-destroy": "^1.0.1",
"@types/sinon": "10.0.11",
"@types/sinon-chai": "3.2.12",
"@types/stringify-object": "^3.0.0",
"chai": "^4.2.0",
"chokidar": "3.6.0",
"fs-extra": "9.1.0",
"mocha": "7.0.1",
"mocha-junit-reporter": "2.2.0",
"mocha-multi-reporters": "1.5.1",
"jest": "^30.1.3",
"nexus": "^1.2.0-next.15",
"sinon": "13.0.2",
"sinon-chai": "3.7.0",
"snap-shot-it": "7.9.10",
"ts-jest": "^29.4.4",
"tslint": "^6.1.3"
},
"files": [
-4
View File
@@ -1,4 +0,0 @@
module.exports = {
spec: 'test/unit/**/*.spec.ts',
watchFiles: ['test/**/*.ts', 'src/**/*.ts'],
}
@@ -1,52 +1,53 @@
import { describe, expect, jest, it } from '@jest/globals'
import type { DataContext } from '../../../src'
import { AuthActions } from '../../../src/actions/AuthActions'
import { createTestDataContext } from '../helper'
import sinon, { SinonStub } from 'sinon'
import sinonChai from 'sinon-chai'
import chai, { expect } from 'chai'
import { FoundBrowser } from '@packages/types'
chai.use(sinonChai)
describe('AuthActions', () => {
context('.login', () => {
describe('.login', () => {
let ctx: DataContext
let actions: AuthActions
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
;(ctx._apis.authApi.logIn as SinonStub)
.resolves({ name: 'steve', email: 'steve@apple.com', authToken: 'foo' })
jest.mocked(ctx._apis.authApi.logIn).mockResolvedValue({ name: 'steve', email: 'steve@apple.com', authToken: 'foo' })
actions = new AuthActions(ctx)
})
it('sets coreData.user', async () => {
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx.coreData.user).to.include({ name: 'steve', email: 'steve@apple.com', authToken: 'foo' })
expect(ctx.coreData.user).toEqual(expect.objectContaining({ name: 'steve', email: 'steve@apple.com', authToken: 'foo' }))
})
it('focuses the main window if there is no activeBrowser', async () => {
ctx.coreData.activeBrowser = null
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
expect(ctx._apis.electronApi.focusMainWindow).toHaveBeenCalledTimes(1)
expect(ctx._apis.browserApi.focusActiveBrowserWindow).not.toHaveBeenCalled()
})
it('focuses the main window if the activeBrowser does not support focus', async () => {
const browser = ctx.coreData.activeBrowser = { name: 'foo' } as FoundBrowser
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(false)
jest.spyOn(ctx.browser, 'isFocusSupported').mockImplementation((args) => {
if (args === browser) {
return Promise.resolve(false)
}
return Promise.resolve(true)
})
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
expect(ctx._apis.electronApi.focusMainWindow).toHaveBeenCalledTimes(1)
expect(ctx._apis.browserApi.focusActiveBrowserWindow).not.toHaveBeenCalled()
})
it('focuses the main window if the activeBrowser supports focus, but browser is closed', async () => {
@@ -54,12 +55,19 @@ describe('AuthActions', () => {
ctx.coreData.app.browserStatus = 'closed'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
jest.spyOn(ctx.browser, 'isFocusSupported').mockImplementation((args) => {
if (args === browser) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
expect(ctx._apis.electronApi.focusMainWindow).toHaveBeenCalledTimes(1)
expect(ctx._apis.browserApi.focusActiveBrowserWindow).not.toHaveBeenCalled()
})
it('focuses the main window if the activeBrowser supports focus, but browser is opening', async () => {
@@ -67,12 +75,19 @@ describe('AuthActions', () => {
ctx.coreData.app.browserStatus = 'opening'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
jest.spyOn(ctx.browser, 'isFocusSupported').mockImplementation((args) => {
if (args === browser) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.be.calledOnce
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
expect(ctx._apis.electronApi.focusMainWindow).toHaveBeenCalledTimes(1)
expect(ctx._apis.browserApi.focusActiveBrowserWindow).not.toHaveBeenCalled()
})
it('focuses the browser if the activeBrowser does support focus and browser is open', async () => {
@@ -80,25 +95,39 @@ describe('AuthActions', () => {
ctx.coreData.app.browserStatus = 'open'
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
jest.spyOn(ctx.browser, 'isFocusSupported').mockImplementation((args) => {
if (args === browser) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.not.be.called
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.be.calledOnce
expect(ctx._apis.electronApi.focusMainWindow).not.toHaveBeenCalled()
expect(ctx._apis.browserApi.focusActiveBrowserWindow).toHaveBeenCalledTimes(1)
})
it('does not focus anything if the activeBrowser does support focus but the main window is focused', async () => {
const browser = ctx.coreData.activeBrowser = { name: 'foo' } as FoundBrowser
sinon.stub(ctx.browser, 'isFocusSupported').withArgs(browser).resolves(true)
jest.spyOn(ctx.browser, 'isFocusSupported').mockImplementation((args) => {
if (args === browser) {
return Promise.resolve(true)
}
;(ctx._apis.electronApi.isMainWindowFocused as SinonStub).returns(true)
return Promise.resolve(false)
})
jest.spyOn(ctx._apis.electronApi, 'isMainWindowFocused').mockReturnValue(true)
// @ts-expect-error - incorrect number of arguments
await actions.login()
expect(ctx._apis.electronApi.focusMainWindow).to.not.be.called
expect(ctx._apis.browserApi.focusActiveBrowserWindow).to.not.be.called
expect(ctx._apis.electronApi.focusMainWindow).not.toHaveBeenCalled()
expect(ctx._apis.browserApi.focusActiveBrowserWindow).not.toHaveBeenCalled()
})
})
})
@@ -1,8 +1,7 @@
import { describe, expect, it, beforeEach } from '@jest/globals'
import type { DataContext } from '../../../src'
import { CodegenActions } from '../../../src/actions/CodegenActions'
import { createTestDataContext } from '../helper'
import { expect } from 'chai'
import sinon from 'sinon'
import path from 'path'
describe('CodegenActions', () => {
@@ -11,128 +10,130 @@ describe('CodegenActions', () => {
let reactDocgen: typeof import('react-docgen')
beforeEach(async () => {
sinon.restore()
ctx = createTestDataContext('open')
reactDocgen = await eval('import("react-docgen")')
actions = new CodegenActions(ctx)
reactDocgen = await eval('import("react-docgen")')
})
context('getReactComponentsFromFile', () => {
const absolutePathPrefix = path.resolve('./test/unit/actions/project')
describe('getReactComponentsFromFile', () => {
let absolutePathPrefix: string
beforeEach(() => {
absolutePathPrefix = path.resolve(__dirname, './project')
})
it('returns React components from file with class component', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-class.jsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('Counter')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('Counter')
expect(components[0].isDefault).toEqual(false)
})
it('returns React components from file with functional component', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-functional.jsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('Counter')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('Counter')
expect(components[0].isDefault).toEqual(false)
})
it('returns only exported React components from file with functional components', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-multiple-components.jsx`, reactDocgen)
expect(components).to.have.length(2)
expect(components[0].exportName).to.equal('CounterContainer')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(2)
expect(components[0].exportName).toEqual('CounterContainer')
expect(components[0].isDefault).toEqual(false)
expect(components[1].exportName).to.equal('CounterView')
expect(components[1].isDefault).to.equal(false)
expect(components[1].exportName).toEqual('CounterView')
expect(components[1].isDefault).toEqual(false)
})
it('returns React components from a tsx file', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter.tsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('Counter')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('Counter')
expect(components[0].isDefault).toEqual(false)
})
it('returns React components that are exported by default', async () => {
let reactComponents = await (await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-default.tsx`, reactDocgen)).components
expect(reactComponents).to.have.length(1)
expect(reactComponents[0].exportName).to.equal('CounterDefault')
expect(reactComponents[0].isDefault).to.equal(true)
expect(reactComponents).toHaveLength(1)
expect(reactComponents[0].exportName).toEqual('CounterDefault')
expect(reactComponents[0].isDefault).toEqual(true)
reactComponents = await (await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-anonymous.jsx`, reactDocgen)).components
expect(reactComponents).to.have.length(1)
expect(reactComponents[0].exportName).to.equal('Component')
expect(reactComponents[0].isDefault).to.equal(true)
expect(reactComponents).toHaveLength(1)
expect(reactComponents[0].exportName).toEqual('Component')
expect(reactComponents[0].isDefault).toEqual(true)
reactComponents = await (await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-function.jsx`, reactDocgen)).components
expect(reactComponents).to.have.length(1)
expect(reactComponents[0].exportName).to.equal('HelloWorld')
expect(reactComponents[0].isDefault).to.equal(true)
expect(reactComponents).toHaveLength(1)
expect(reactComponents[0].exportName).toEqual('HelloWorld')
expect(reactComponents[0].isDefault).toEqual(true)
reactComponents = await (await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-class.jsx`, reactDocgen)).components
expect(reactComponents).to.have.length(1)
expect(reactComponents[0].exportName).to.equal('HelloWorld')
expect(reactComponents[0].isDefault).to.equal(true)
expect(reactComponents).toHaveLength(1)
expect(reactComponents[0].exportName).toEqual('HelloWorld')
expect(reactComponents[0].isDefault).toEqual(true)
reactComponents = await (await actions.getReactComponentsFromFile(`${absolutePathPrefix}/default-specifier.jsx`, reactDocgen)).components
expect(reactComponents).to.have.length(1)
expect(reactComponents[0].exportName).to.equal('HelloWorld')
expect(reactComponents[0].isDefault).to.equal(true)
expect(reactComponents).toHaveLength(1)
expect(reactComponents[0].exportName).toEqual('HelloWorld')
expect(reactComponents[0].isDefault).toEqual(true)
})
it('returns React components defined with arrow functions', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-arrow-function.jsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('Counter')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('Counter')
expect(components[0].isDefault).toEqual(false)
})
it('returns React components from a file with multiple separate export statements', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-separate-exports.jsx`, reactDocgen)
expect(components).to.have.length(2)
expect(components[0].exportName).to.equal('CounterView')
expect(components[0].isDefault).to.equal(false)
expect(components[1].exportName).to.equal('CounterContainer')
expect(components[1].isDefault).to.equal(true)
expect(components).toHaveLength(2)
expect(components[0].exportName).toEqual('CounterView')
expect(components[0].isDefault).toEqual(false)
expect(components[1].exportName).toEqual('CounterContainer')
expect(components[1].isDefault).toEqual(true)
})
it('returns React components that are exported and aliased', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/export-alias.jsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('HelloWorld')
expect(components[0].isDefault).to.equal(false)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('HelloWorld')
expect(components[0].isDefault).toEqual(false)
})
// TODO: "react-docgen" will resolve HOCs but our export detection does not. Can fall back to displayName here
it.skip('handles higher-order-components', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/counter-hoc.jsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('Counter')
expect(components[0].isDefault).to.equal(true)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('Counter')
expect(components[0].isDefault).toEqual(true)
})
it('correctly parses typescript files', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/LoginForm.tsx`, reactDocgen)
expect(components).to.have.length(1)
expect(components[0].exportName).to.equal('LoginForm')
expect(components[0].isDefault).to.equal(true)
expect(components).toHaveLength(1)
expect(components[0].exportName).toEqual('LoginForm')
expect(components[0].isDefault).toEqual(true)
})
it('does not throw while parsing empty file', async () => {
const { components } = await actions.getReactComponentsFromFile(`${absolutePathPrefix}/empty.jsx`, reactDocgen)
expect(components).to.have.length(0)
expect(components).toHaveLength(0)
})
it('ensure that Babel is instructed to not use a config file', async () => {
@@ -152,11 +153,11 @@ describe('CodegenActions', () => {
},
}
const filePath = path.join(process.cwd(), 'test/unit/actions/project/counter-class.jsx')
const filePath = path.join(__dirname, 'project/counter-class.jsx')
await actions.getReactComponentsFromFile(filePath, mockReactDocgen)
await actions.getReactComponentsFromFile(filePath, mockReactDocgen as unknown as typeof import('react-docgen'))
expect(capturedOptions.babelOptions.configFile).to.equal(false)
expect(capturedOptions.babelOptions.configFile).toEqual(false)
})
})
})
@@ -1,29 +1,26 @@
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import { CohortsActions } from '../../../src/actions/CohortsActions'
import { createTestDataContext } from '../helper'
import { expect } from 'chai'
import sinon, { SinonStub, match } from 'sinon'
describe('CohortsActions', () => {
let ctx: DataContext
let actions: CohortsActions
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
actions = new CohortsActions(ctx)
})
context('getCohort', () => {
describe('getCohort', () => {
it('should return null if name not found', async () => {
const name = '123'
const cohort = await actions.getCohort(name)
expect(cohort).to.be.undefined
expect(ctx.config.cohortsApi.getCohort).to.have.been.calledWith(name)
expect(cohort).toBeUndefined()
expect(ctx.config.cohortsApi.getCohort).toHaveBeenCalledWith(name)
})
it('should return cohort if in cache', async () => {
@@ -32,16 +29,16 @@ describe('CohortsActions', () => {
cohort: 'A',
}
;(ctx._apis.cohortsApi.getCohort as SinonStub).resolves(cohort)
jest.spyOn(ctx._apis.cohortsApi, 'getCohort').mockResolvedValue(cohort)
const cohortReturned = await actions.getCohort(cohort.name)
expect(cohortReturned).to.eq(cohort)
expect(ctx.config.cohortsApi.getCohort).to.have.been.calledWith(cohort.name)
expect(cohortReturned).toEqual(cohort)
expect(ctx.config.cohortsApi.getCohort).toHaveBeenCalledWith(cohort.name)
})
})
context('determineCohort', () => {
describe('determineCohort', () => {
it('should determine cohort', async () => {
const cohortConfig = {
name: 'loginBanner',
@@ -50,8 +47,8 @@ describe('CohortsActions', () => {
const pickedCohort = await actions.determineCohort(cohortConfig.name, cohortConfig.cohorts)
expect(ctx.config.cohortsApi.insertCohort).to.have.been.calledOnceWith({ name: cohortConfig.name, cohort: match.string })
expect(cohortConfig.cohorts.includes(pickedCohort.cohort)).to.be.true
expect(ctx.config.cohortsApi.insertCohort).toHaveBeenNthCalledWith(1, { name: cohortConfig.name, cohort: expect.any(String) })
expect(cohortConfig.cohorts.includes(pickedCohort.cohort)).toBe(true)
})
})
})
@@ -1,14 +1,13 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { describe, expect, it, beforeAll, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import { DataEmitterActions } from '../../../src/actions/DataEmitterActions'
import { createTestDataContext } from '../helper'
describe('DataEmitterActions', () => {
context('.subscribeTo', () => {
describe('.subscribeTo', () => {
let ctx: DataContext
before(() => {
beforeAll(() => {
ctx = createTestDataContext('open')
})
@@ -44,8 +43,8 @@ describe('DataEmitterActions', () => {
await iteratorPromise
expect(items).to.eql(3)
expect(completed).to.be.true
expect(items).toEqual(3)
expect(completed).toBe(true)
})
it('handles iterating through events if an event is emitted before the iteration', async () => {
@@ -75,8 +74,8 @@ describe('DataEmitterActions', () => {
await iteratorPromise
expect(items).to.eql(3)
expect(completed).to.be.true
expect(items).toEqual(3)
expect(completed).toBe(true)
})
it('handles stopping the loop if return is called before the iteration', async () => {
@@ -103,8 +102,8 @@ describe('DataEmitterActions', () => {
await iteratorPromise
expect(items).to.eql(0)
expect(completed).to.be.true
expect(items).toEqual(0)
expect(completed).toBe(true)
})
const createTestIterator = (subscription) => {
@@ -130,7 +129,7 @@ describe('DataEmitterActions', () => {
it('handles multiple subscriptions', async () => {
const actions = new DataEmitterActions(ctx)
const unsubscribe1 = sinon.stub()
const unsubscribe1 = jest.fn()
const subscription1 = actions.subscribeTo('specsChange', { sendInitial: true, onUnsubscribe: unsubscribe1 })
@@ -146,7 +145,7 @@ describe('DataEmitterActions', () => {
let subscription2
let iteratorFactory2
let iteratorPromise2
let unsubscribe2 = sinon.stub()
let unsubscribe2 = jest.fn()
setImmediate(() => {
subscription2 = actions.subscribeTo('specsChange', { sendInitial: true, onUnsubscribe: unsubscribe2 })
@@ -168,14 +167,17 @@ describe('DataEmitterActions', () => {
await iteratorPromise1
await iteratorPromise2
expect(iteratorFactory1.items, 'first subscription should be called 3 times').to.eql(3)
expect(iteratorFactory1.completed).to.be.true
// first subscription should be called 3 times
expect(iteratorFactory1.items).toEqual(3)
expect(iteratorFactory1.completed).toBe(true)
expect(iteratorFactory2.items, 'second subscription should be called 2 times').to.eql(2)
expect(iteratorFactory2.completed).to.be.true
// second subscription should be called 2 times
expect(iteratorFactory2.items).toEqual(2)
expect(iteratorFactory2.completed).toBe(true)
expect(unsubscribe1, 'should unsubscribe and see there is 1 subscription still listening').to.have.been.calledOnceWith(1)
expect(unsubscribe2).to.have.been.calledOnceWith(0)
// should unsubscribe and see there is 1 subscription still listening
expect(unsubscribe1).toHaveBeenNthCalledWith(1, 1)
expect(unsubscribe2).toHaveBeenNthCalledWith(1, 0)
})
})
})
@@ -1,29 +1,23 @@
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import { EventCollectorActions } from '../../../src/actions/EventCollectorActions'
import { createTestDataContext } from '../helper'
import sinon, { SinonStub } from 'sinon'
import sinonChai from 'sinon-chai'
import chai, { expect } from 'chai'
const pkg = require('@packages/root')
chai.use(sinonChai)
import pkg from '@packages/root'
describe('EventCollectorActions', () => {
let ctx: DataContext
let actions: EventCollectorActions
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
sinon.stub(ctx.util, 'fetch').resolves({} as any)
jest.spyOn(ctx.util, 'fetch').mockResolvedValue({} as any)
actions = new EventCollectorActions(ctx)
})
context('.recordEvent', () => {
describe('.recordEvent', () => {
it('makes expected request for anonymous event', async () => {
await actions.recordEvent({
campaign: 'abc',
@@ -32,8 +26,9 @@ describe('EventCollectorActions', () => {
cohort: '123',
}, false)
expect(ctx.util.fetch).to.have.been.calledOnceWith(
sinon.match(/anon-collect$/), // Verify URL ends with expected 'anon-collect' path
expect(ctx.util.fetch).toHaveBeenNthCalledWith(
1,
expect.stringMatching(/anon-collect$/), // Verify URL ends with expected 'anon-collect' path
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'x-cypress-version': pkg.version }, body: '{"campaign":"abc","medium":"def","messageId":"ghi","cohort":"123"}' },
)
})
@@ -48,26 +43,27 @@ describe('EventCollectorActions', () => {
cohort: '123',
}, true)
expect(ctx.util.fetch).to.have.been.calledOnceWith(
sinon.match(/machine-collect$/), // Verify URL ends with expected 'machine-collect' path
expect(ctx.util.fetch).toHaveBeenNthCalledWith(
1,
expect.stringMatching(/machine-collect$/), // Verify URL ends with expected 'machine-collect' path
{ method: 'POST', headers: { 'Content-Type': 'application/json', 'x-cypress-version': pkg.version }, body: '{"campaign":"abc","medium":"def","messageId":"ghi","cohort":"123","machineId":"xyz"}' },
)
})
it('resolve true if request succeeds', async () => {
(ctx.util.fetch as SinonStub).resolves({} as any)
jest.spyOn(ctx.util, 'fetch').mockResolvedValue({} as any)
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false)
expect(result).to.eql(true)
expect(result).toBe(true)
})
it('resolves false if request fails', async () => {
(ctx.util.fetch as SinonStub).rejects({} as any)
jest.spyOn(ctx.util, 'fetch').mockRejectedValue({} as any)
const result = await actions.recordEvent({ campaign: '', medium: '', messageId: '', cohort: '' }, false)
expect(result).to.eql(false)
expect(result).toBe(false)
})
})
})
@@ -1,5 +1,4 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { describe, expect, it, jest } from '@jest/globals'
import { LocalSettingsActions } from '../../../src/actions/LocalSettingsActions'
import { createTestDataContext } from '../helper'
import type { DataContext } from '../../../src'
@@ -10,54 +9,51 @@ describe('LocalSettingsActions', () => {
let actions: LocalSettingsActions
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
actions = new LocalSettingsActions(ctx)
})
context('refreshLocalSettings', () => {
context('notifyWhenRunCompletes', () => {
describe('refreshLocalSettings', () => {
describe('notifyWhenRunCompletes', () => {
it('should fix false value', async () => {
ctx._apis.localSettingsApi.getPreferences = sinon.stub().resolves({
//@ts-ignore
jest.spyOn(ctx._apis.localSettingsApi, 'getPreferences').mockResolvedValue({
// @ts-expect-error - incorrect return type
notifyWhenRunCompletes: false,
})
await actions.refreshLocalSettings()
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).to.eql([])
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).toEqual([])
})
it('should fix true value', async () => {
ctx._apis.localSettingsApi.getPreferences = sinon.stub().resolves({
//@ts-ignore
jest.spyOn(ctx._apis.localSettingsApi, 'getPreferences').mockResolvedValue({
// @ts-expect-error - incorrect return type
notifyWhenRunCompletes: true,
})
await actions.refreshLocalSettings()
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).to.eql([...NotifyCompletionStatuses])
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).toEqual([...NotifyCompletionStatuses])
})
it('should leave value alone if value is an array', async () => {
ctx._apis.localSettingsApi.getPreferences = sinon.stub().resolves({
//@ts-ignore
jest.spyOn(ctx._apis.localSettingsApi, 'getPreferences').mockResolvedValue({
notifyWhenRunCompletes: ['errored'],
})
await actions.refreshLocalSettings()
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).to.eql(['errored'])
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).toEqual(['errored'])
})
it('should pass through default value if not set ', async () => {
ctx._apis.localSettingsApi.getPreferences = sinon.stub().resolves({})
jest.spyOn(ctx._apis.localSettingsApi, 'getPreferences').mockResolvedValue({})
await actions.refreshLocalSettings()
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).to.eql(['failed'])
expect(ctx.coreData.localSettings.preferences.notifyWhenRunCompletes).toEqual(['failed'])
})
})
})
@@ -1,5 +1,4 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import { NotificationActions } from '../../../src/actions/NotificationActions'
import { CloudRunStatus, RelevantRunInfo } from '../../../src/gen/graphcache-config.gen'
@@ -11,43 +10,43 @@ describe('NotificationActions', () => {
let showSystemNotificationStub
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
actions = new NotificationActions(ctx)
ctx.coreData.currentProject = '/cy-project'
showSystemNotificationStub = sinon.stub(ctx.actions.electron, 'showSystemNotification')
sinon.stub(ctx.actions.cloudProject, 'fetchMetadata').resolves({
showSystemNotificationStub = jest.spyOn(ctx.actions.electron, 'showSystemNotification')
jest.spyOn(ctx.actions.cloudProject, 'fetchMetadata').mockResolvedValue({
id: 'project-local-id',
name: 'cy-project',
})
})
context('onNotificationClick', () => {
describe('onNotificationClick', () => {
it('focuses the active browser window and calls debugCloudRun', async () => {
const run = createRelevantRun(12)
const focusActiveBrowserWindowSpy = sinon.spy(ctx.actions.browser, 'focusActiveBrowserWindow')
const focusActiveBrowserWindowSpy = jest.spyOn(ctx.actions.browser, 'focusActiveBrowserWindow')
const debugCloudRunSpy = sinon.spy(ctx.actions.project, 'debugCloudRun')
const debugCloudRunSpy = jest.spyOn(ctx.actions.project, 'debugCloudRun')
await actions.onNotificationClick(run)
expect(focusActiveBrowserWindowSpy).to.have.been.called
expect(debugCloudRunSpy).to.have.been.calledWith(run.runNumber)
expect(focusActiveBrowserWindowSpy).toHaveBeenCalled()
expect(debugCloudRunSpy).toHaveBeenCalledWith(run.runNumber)
})
})
context('sendRunStartedNotification', () => {
describe('sendRunStartedNotification', () => {
it('does not send notification if preference is not enabled', async () => {
ctx.coreData.localSettings.preferences.notifyWhenRunStarts = false
// @ts-expect-error - number as arg
await actions.sendRunStartedNotification(101)
expect(showSystemNotificationStub).not.to.have.been.called
expect(showSystemNotificationStub).not.toHaveBeenCalled()
})
it('sends notification if preference is enabled', async () => {
@@ -57,11 +56,11 @@ describe('NotificationActions', () => {
await actions.sendRunStartedNotification(run)
expect(showSystemNotificationStub).to.have.been.calledWithMatch('cy-project', `Run #${run.runNumber} started`)
expect(showSystemNotificationStub).toHaveBeenCalledWith('cy-project', `Run #${run.runNumber} started`, expect.any(Function))
})
})
context('sendRunFailingNotification', () => {
describe('sendRunFailingNotification', () => {
it('does not send notification if preference is not enabled', () => {
const run = createRelevantRun(101)
@@ -69,7 +68,7 @@ describe('NotificationActions', () => {
actions.sendRunFailingNotification(run)
expect(showSystemNotificationStub).not.to.have.been.called
expect(showSystemNotificationStub).not.toHaveBeenCalled()
})
it('sends notification if preference is enabled', async () => {
@@ -79,17 +78,18 @@ describe('NotificationActions', () => {
await actions.sendRunFailingNotification(run)
expect(showSystemNotificationStub).to.have.been.calledWithMatch('cy-project', `Run #${run.runNumber} has started failing`)
expect(showSystemNotificationStub).toHaveBeenCalledWith('cy-project', `Run #${run.runNumber} has started failing`, expect.any(Function))
})
})
context('sendRunCompletedNotification', () => {
describe('sendRunCompletedNotification', () => {
it('does not send notification if status is not included in preference', () => {
ctx.coreData.localSettings.preferences.notifyWhenRunCompletes = ['cancelled', 'errored', 'failed']
// @ts-expect-error - number as arg
actions.sendRunCompletedNotification(101, 'passed')
expect(showSystemNotificationStub).not.to.have.been.called
expect(showSystemNotificationStub).not.toHaveBeenCalled()
})
it('sends notification if preference is enabled', async () => {
@@ -99,11 +99,11 @@ describe('NotificationActions', () => {
await actions.sendRunCompletedNotification(run, 'failed')
expect(showSystemNotificationStub).to.have.been.calledWithMatch('cy-project', `Run #${run.runNumber} failed`)
expect(showSystemNotificationStub).toHaveBeenCalledWith('cy-project', `Run #${run.runNumber} failed`, expect.any(Function))
})
})
context('maybeSendRunNotification', () => {
describe('maybeSendRunNotification', () => {
beforeEach(() => {
// For these tests, enable all notification preferences to verify that desktopNotificationsEnabled works as expected
ctx.coreData.localSettings.preferences.notifyWhenRunStarts = true
@@ -119,11 +119,11 @@ describe('NotificationActions', () => {
{ ...createRelevantRun(141), status: 'PASSED', sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 1 },
)
expect(showSystemNotificationStub).not.to.have.been.called
expect(showSystemNotificationStub).not.toHaveBeenCalled()
})
it('sends run started notification if there is a new run with RUNNING status that is different from the previously cached run', () => {
const sendRunStartedNotificationStub = sinon.stub(actions, 'sendRunStartedNotification')
const sendRunStartedNotificationStub = jest.spyOn(actions, 'sendRunStartedNotification')
ctx.coreData.localSettings.preferences.desktopNotificationsEnabled = true
const run1 = { ...createRelevantRun(141), status: 'RUNNING', sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 1 } as const
@@ -133,11 +133,11 @@ describe('NotificationActions', () => {
run1, run2,
)
expect(sendRunStartedNotificationStub).to.have.been.calledWith(run2)
expect(sendRunStartedNotificationStub).toHaveBeenCalledWith(run2)
})
it('sends run started failing notification if status is RUNNING and totalFailed was 0 but is now greater than 0', () => {
const sendRunFailingNotificationStub = sinon.stub(actions, 'sendRunFailingNotification')
const sendRunFailingNotificationStub = jest.spyOn(actions, 'sendRunFailingNotification')
const run1 = { ...createRelevantRun(141), status: 'RUNNING', sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 0 } as const
const run2 = { ...createRelevantRun(141), status: 'RUNNING', sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 3 } as const
@@ -145,21 +145,22 @@ describe('NotificationActions', () => {
actions.maybeSendRunNotification(run1, run2)
expect(sendRunFailingNotificationStub).to.have.been.calledWith(run2)
expect(sendRunFailingNotificationStub).toHaveBeenCalledWith(run2)
})
context('run completed', () => {
describe('run completed', () => {
['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'].forEach((status) => {
it(`sends run completed notification if new run has completed - ${status}`, () => {
const run1: RelevantRunInfo = { ...createRelevantRun(141), status: 'RUNNING', sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 0, branch: 'branch123', organizationId: '1' }
const run2: RelevantRunInfo = { ...createRelevantRun(142), status: status as CloudRunStatus, sha: 'f909139209c8351cfaa737c7fd122ad4f17fdaa5', totalFailed: 0, branch: 'branch123', organizationId: '1' }
const sendRunCompletedNotificationStub = sinon.stub(actions, 'sendRunCompletedNotification')
const sendRunCompletedNotificationStub = jest.spyOn(actions, 'sendRunCompletedNotification')
ctx.coreData.localSettings.preferences.desktopNotificationsEnabled = true
actions.maybeSendRunNotification(run1, run2)
expect(sendRunCompletedNotificationStub).to.have.been.calledWith(run2, status.toLocaleLowerCase())
// @ts-expect-error
expect(sendRunCompletedNotificationStub).toHaveBeenCalledWith(run2, status.toLocaleLowerCase())
})
})
})
@@ -1,34 +1,31 @@
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import { ProjectActions } from '../../../src/actions/ProjectActions'
import { createTestDataContext } from '../helper'
import { expect } from 'chai'
import sinon from 'sinon'
import { SpecWithRelativeRoot } from '@packages/types'
import { SpecWithRelativeRoot, TestingType } from '@packages/types'
describe('ProjectActions', () => {
let ctx: DataContext
let actions: ProjectActions
beforeEach(() => {
sinon.restore()
ctx = createTestDataContext('open')
actions = new ProjectActions(ctx)
})
context('hasNonExampleSpec', () => {
context('testing type not set yet', () => {
describe('hasNonExampleSpec', () => {
describe('testing type not set yet', () => {
it('should indicate there are NO non example spec files if empty', async () => {
expect(ctx.project.specs).to.have.length(0)
expect(ctx.project.specs).toHaveLength(0)
const hasNonExampleSpec = await actions.hasNonExampleSpec()
expect(hasNonExampleSpec).to.be.false
expect(hasNonExampleSpec).toBe(false)
})
})
context('testing type is e2e', () => {
describe('testing type is e2e', () => {
beforeEach(() => {
ctx.coreData.currentTestingType = 'e2e'
})
@@ -43,11 +40,11 @@ describe('ProjectActions', () => {
ctx.project.setSpecs(mockSpecs)
expect(ctx.project.specs).to.have.length(1)
expect(ctx.project.specs).toHaveLength(1)
const hasNonExampleSpec = await actions.hasNonExampleSpec()
expect(hasNonExampleSpec).to.be.false
expect(hasNonExampleSpec).toBe(false)
})
it('should indicate there are non example spec files with examples and non example', async () => {
@@ -64,26 +61,26 @@ describe('ProjectActions', () => {
ctx.project.setSpecs(mockSpecs)
expect(ctx.project.specs).to.have.length(2)
expect(ctx.project.specs).toHaveLength(2)
const hasNonExampleSpec = await actions.hasNonExampleSpec()
expect(hasNonExampleSpec).to.be.true
expect(hasNonExampleSpec).toBe(true)
})
})
context('testing type is component', () => {
describe('testing type is component', () => {
it('should indicate there are NO non example spec files with no specs', async () => {
const mockSpecs = [] as SpecWithRelativeRoot[]
ctx.coreData.currentTestingType = 'component'
ctx.project.setSpecs(mockSpecs)
expect(ctx.project.specs).to.have.length(0)
expect(ctx.project.specs).toHaveLength(0)
const hasNonExampleSpec = await actions.hasNonExampleSpec()
expect(hasNonExampleSpec).to.be.false
expect(hasNonExampleSpec).toBe(false)
})
// there are no examples for component tests, so any component spec file should be a non-example
@@ -95,28 +92,28 @@ describe('ProjectActions', () => {
ctx.coreData.currentTestingType = 'component'
ctx.project.setSpecs(mockSpecs)
expect(ctx.project.specs).to.have.length(1)
expect(ctx.project.specs).toHaveLength(1)
const hasNonExampleSpec = await actions.hasNonExampleSpec()
expect(hasNonExampleSpec).to.be.true
expect(hasNonExampleSpec).toBe(true)
})
})
})
describe('runSpec', () => {
context('no project', () => {
describe('no project', () => {
it('should fail with `NO_PROJECT`', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/cypress/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
code: 'NO_PROJECT',
detailMessage: sinon.match.string,
detailMessage: expect.any(String),
})
})
})
context('empty specPath', () => {
describe('empty specPath', () => {
beforeEach(() => {
ctx.coreData.currentProject = '/cy-project'
})
@@ -124,125 +121,166 @@ describe('ProjectActions', () => {
it('should fail with `NO_SPEC_PATH`', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '' })
sinon.assert.match(result, {
expect(result).toMatchObject({
code: 'NO_SPEC_PATH',
detailMessage: sinon.match.string,
detailMessage: expect.any(String),
})
})
})
context('no specPattern match', () => {
describe('no specPattern match', () => {
beforeEach(() => {
ctx.coreData.currentProject = '/cy-project'
sinon.stub(ctx.project, 'matchesSpecPattern').resolves(false)
jest.spyOn(ctx.project, 'matchesSpecPattern').mockResolvedValue(false)
})
it('should fail with `NO_SPEC_PATTERN_MATCH`', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
code: 'NO_SPEC_PATTERN_MATCH',
detailMessage: sinon.match.string,
detailMessage: expect.any(String),
})
})
})
context('spec file not found', () => {
describe('spec file not found', () => {
beforeEach(() => {
ctx.coreData.currentProject = '/cy-project'
sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true)
sinon.stub(ctx.fs, 'existsSync').returns(false)
jest.spyOn(ctx.project, 'matchesSpecPattern').mockImplementation((specFile: string) => {
if (specFile.includes('e2e')) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
jest.spyOn(ctx.fs, 'existsSync').mockReturnValue(false)
})
it('should fail with `SPEC_NOT_FOUND`', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
code: 'SPEC_NOT_FOUND',
detailMessage: sinon.match.string,
detailMessage: expect.any(String),
})
})
})
context('matched testing type not configured', () => {
describe('matched testing type not configured', () => {
beforeEach(() => {
ctx.coreData.currentTestingType = null
ctx.coreData.currentProject = '/cy-project'
sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true)
sinon.stub(ctx.fs, 'existsSync').returns(true)
sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(false)
jest.spyOn(ctx.project, 'matchesSpecPattern').mockImplementation((specFile: string) => {
if (specFile.includes('e2e')) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
jest.spyOn(ctx.fs, 'existsSync').mockReturnValue(true)
jest.spyOn(ctx.lifecycleManager, 'isTestingTypeConfigured').mockImplementation((testingType: TestingType) => {
if (testingType === 'e2e') {
return false
}
return true
})
})
it('should fail with `TESTING_TYPE_NOT_CONFIGURED`', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
code: 'TESTING_TYPE_NOT_CONFIGURED',
detailMessage: sinon.match.string,
detailMessage: expect.any(String),
})
})
})
context('spec can be executed', () => {
describe('spec can be executed', () => {
beforeEach(() => {
ctx.coreData.currentProject = '/cy-project'
sinon.stub(ctx.project, 'matchesSpecPattern').withArgs(sinon.match.string, 'e2e').resolves(true)
sinon.stub(ctx.fs, 'existsSync').returns(true)
sinon.stub(ctx.project, 'getCurrentSpecByAbsolute').returns({ id: 'xyz' } as any)
sinon.stub(ctx.lifecycleManager, 'setInitialActiveBrowser')
jest.spyOn(ctx.project, 'matchesSpecPattern').mockImplementation((specFile: string) => {
if (specFile.includes('e2e')) {
return Promise.resolve(true)
}
return Promise.resolve(false)
})
jest.spyOn(ctx.fs, 'existsSync').mockReturnValue(true)
jest.spyOn(ctx.project, 'getCurrentSpecByAbsolute').mockReturnValue({ id: 'xyz' } as any)
jest.spyOn(ctx.lifecycleManager, 'setInitialActiveBrowser')
ctx.coreData.activeBrowser = { id: 'abc' } as any
sinon.stub(ctx.lifecycleManager, 'setCurrentTestingType')
sinon.stub(ctx.actions.project, 'switchTestingTypesAndRelaunch')
jest.spyOn(ctx.lifecycleManager, 'setCurrentTestingType').mockReturnValue(undefined)
jest.spyOn(ctx.lifecycleManager, 'setAndLoadCurrentTestingType').mockReturnValue(undefined)
jest.spyOn(ctx.actions.project, 'switchTestingTypesAndRelaunch')
jest.spyOn(ctx.actions.browser, 'closeBrowser').mockReturnValue(undefined)
ctx.coreData.app.browserStatus = 'open'
sinon.stub(ctx.emitter, 'subscribeTo').returns({
jest.spyOn(ctx.emitter, 'subscribeTo').mockReturnValue({
next: () => {},
return: () => {},
} as any)
})
context('no current testing type', () => {
describe('no current testing type', () => {
beforeEach(() => {
ctx.coreData.currentTestingType = null
sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(true)
jest.spyOn(ctx.lifecycleManager, 'isTestingTypeConfigured').mockImplementation((testingType: TestingType) => {
if (testingType === 'e2e') {
return true
}
return false
})
})
it('should succeed', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
testingType: 'e2e',
browser: sinon.match.object,
spec: sinon.match.object,
browser: expect.any(Object),
spec: expect.any(Object),
})
expect(ctx.lifecycleManager.setCurrentTestingType).to.have.been.calledWith('e2e')
expect(ctx.actions.project.switchTestingTypesAndRelaunch).to.have.been.calledWith('e2e')
expect(ctx._apis.projectApi.runSpec).to.have.been.called
expect(ctx.lifecycleManager.setCurrentTestingType).toHaveBeenCalledWith('e2e')
expect(ctx.actions.project.switchTestingTypesAndRelaunch).toHaveBeenCalledWith('e2e')
expect(ctx._apis.projectApi.runSpec).toHaveBeenCalled()
})
})
context('testing type needs to change', () => {
describe('testing type needs to change', () => {
beforeEach(() => {
ctx.coreData.currentTestingType = 'component'
sinon.stub(ctx.lifecycleManager, 'isTestingTypeConfigured').withArgs('e2e').returns(true)
jest.spyOn(ctx.lifecycleManager, 'isTestingTypeConfigured').mockImplementation((testingType: TestingType) => {
if (testingType === 'e2e') {
return true
}
return false
})
})
it('should succeed', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
testingType: 'e2e',
browser: sinon.match.object,
spec: sinon.match.object,
browser: expect.any(Object),
spec: expect.any(Object),
})
expect(ctx.lifecycleManager.setCurrentTestingType).to.have.been.calledWith('e2e')
expect(ctx.actions.project.switchTestingTypesAndRelaunch).to.have.been.calledWith('e2e')
expect(ctx._apis.projectApi.runSpec).to.have.been.called
expect(ctx.lifecycleManager.setCurrentTestingType).toHaveBeenCalledWith('e2e')
expect(ctx.actions.project.switchTestingTypesAndRelaunch).toHaveBeenCalledWith('e2e')
expect(ctx._apis.projectApi.runSpec).toHaveBeenCalled()
})
})
context('testing type does not need to change', () => {
describe('testing type does not need to change', () => {
beforeEach(() => {
ctx.coreData.currentTestingType = 'e2e'
})
@@ -250,16 +288,16 @@ describe('ProjectActions', () => {
it('should succeed', async () => {
const result = await ctx.actions.project.runSpec({ specPath: '/Users/blah/Desktop/application/e2e/abc.cy.ts' })
sinon.assert.match(result, {
expect(result).toMatchObject({
testingType: 'e2e',
browser: sinon.match.object,
spec: sinon.match.object,
browser: expect.any(Object),
spec: expect.any(Object),
})
expect(ctx.lifecycleManager.setCurrentTestingType).not.to.have.been.called
expect(ctx.actions.project.switchTestingTypesAndRelaunch).not.to.have.been.called
expect(ctx.lifecycleManager.setCurrentTestingType).not.toHaveBeenCalled()
expect(ctx.actions.project.switchTestingTypesAndRelaunch).not.toHaveBeenCalled()
expect(ctx._apis.projectApi.runSpec).to.have.been.called
expect(ctx._apis.projectApi.runSpec).toHaveBeenCalled()
})
})
})
@@ -267,14 +305,14 @@ describe('ProjectActions', () => {
describe('debugCloudRun', () => {
beforeEach(() => {
sinon.stub(ctx.relevantRuns, 'moveToRun')
jest.spyOn(ctx.relevantRuns, 'moveToRun')
})
it('should call moveToRun and routeToDebug', async () => {
await ctx.actions.project.debugCloudRun(123)
expect(ctx.relevantRuns.moveToRun).to.have.been.calledWith(123)
expect(ctx._apis.projectApi.routeToDebug).to.have.been.called
expect(ctx.relevantRuns.moveToRun).toHaveBeenCalledWith(123, [])
expect(ctx._apis.projectApi.routeToDebug).toHaveBeenCalled()
})
})
})
@@ -1,5 +1,5 @@
import { describe, expect, it, beforeEach } from '@jest/globals'
import { parse } from '@babel/parser'
import { expect } from 'chai'
import dedent from 'dedent'
import fs from 'fs-extra'
import path from 'path'
@@ -73,7 +73,7 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const skippedExpected = {
...expected,
@@ -81,7 +81,7 @@ describe('code-generator', () => {
}
const skippedCodeGenResults = await codeGenerator(action, codeGenArgs)
expect(skippedCodeGenResults).deep.eq(skippedExpected)
expect(skippedCodeGenResults).toEqual(skippedExpected)
const getMTimes = (files: Array<CodeGenResult>) => {
return Promise.all(
@@ -91,7 +91,7 @@ describe('code-generator', () => {
const mTimesBefore = await getMTimes(codeGenResults.files)
let mTimesAfter = await getMTimes(skippedCodeGenResults.files)
expect(mTimesBefore).deep.eq(mTimesAfter)
expect(mTimesBefore).toEqual(mTimesAfter)
const overwriteAction: Action = { ...action, overwrite: true }
const overwriteExpected: CodeGenResults = {
@@ -103,11 +103,11 @@ describe('code-generator', () => {
codeGenArgs,
)
expect(overwriteCodeGenResults).deep.eq(overwriteExpected)
expect(overwriteCodeGenResults).toEqual(overwriteExpected)
mTimesAfter = await getMTimes(overwriteCodeGenResults.files)
expect(mTimesBefore).not.deep.eq(mTimesAfter)
expect(mTimesBefore).not.toEqual(mTimesAfter)
mTimesAfter.forEach((time, i) => expect(time > mTimesBefore[i]))
})
@@ -139,13 +139,13 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(fileContent).toEqual(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
expect(() => babelParse(fileContent)).not.toThrow()
})
it('should generate from empty component template', async () => {
@@ -178,13 +178,13 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(fileContent).toEqual(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
expect(() => babelParse(fileContent)).not.toThrow()
})
it('should generate from Vue component template', async () => {
@@ -221,13 +221,13 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(fileContent).toEqual(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
expect(() => babelParse(fileContent)).not.toThrow()
})
it('should generate from React component template', async () => {
@@ -268,13 +268,13 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(fileContent).toEqual(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
expect(() => babelParse(fileContent)).not.toThrow()
})
it('should generate from React component template with default export', async () => {
@@ -315,13 +315,13 @@ describe('code-generator', () => {
failed: [],
}
expect(codeGenResults).deep.eq(expected)
expect(codeGenResults).toEqual(expected)
const fileContent = (await fs.readFile(fileAbsolute)).toString()
expect(fileContent).eq(expected.files[0].content)
expect(fileContent).toEqual(expected.files[0].content)
expect(() => babelParse(fileContent)).not.throw()
expect(() => babelParse(fileContent)).not.toThrow()
})
it('should generate from e2eExamples', async () => {
@@ -335,13 +335,13 @@ describe('code-generator', () => {
// https://github.com/cypress-io/cypress-example-kitchensink
const codeGenResult = await codeGenerator(action, {})
expect(codeGenResult.files.length).gt(0)
expect(codeGenResult.files.length).toBeGreaterThan(0)
for (const res of codeGenResult.files) {
expect(async () => await fs.access(res.file, fs.constants.F_OK)).not.throw()
expect(fs.access(res.file, fs.constants.F_OK)).resolves.not.toThrow()
const shouldParse = ['js', 'ts'].some((ext) => res.file.endsWith(ext))
if (shouldParse) {
expect(() => babelParse(res.content)).not.throw()
expect(() => babelParse(res.content)).not.toThrow()
}
}
})
@@ -357,6 +357,7 @@ describe('code-generator', () => {
currentProject: 'path/to/myProject',
codeGenPath: path.join(__dirname, 'files', 'react', 'Button.jsx'),
codeGenType: 'component',
// @ts-expect-error
framework: CT_FRAMEWORKS[1],
isDefaultSpecPattern: true,
specPattern: [defaultSpecPattern.component],
@@ -366,16 +367,17 @@ describe('code-generator', () => {
const codeGenResult = await codeGenerator(action, codeGenOptions)
expect(() => babelParse(codeGenResult.files[0].content)).not.throw()
expect(() => babelParse(codeGenResult.files[0].content)).not.toThrow()
})
context('nonExampleSpecfile', () => {
describe('nonExampleSpecfile', () => {
it('should return true after adding new spec file', async () => {
const target = path.join(tmpPath, 'spec-check')
const checkBeforeScaffolding = await hasNonExampleSpec(templates.e2eExamples, [])
expect(checkBeforeScaffolding, 'expected having no spec files to show no non-example specs').to.be.false
// expected having no spec files to show no non-example specs
expect(checkBeforeScaffolding).toBe(false)
const scaffoldExamplesAction: Action = {
templateDir: templates.e2eExamples,
@@ -390,13 +392,15 @@ describe('code-generator', () => {
const scaffoldResults = await codeGenerator(scaffoldExamplesAction, {})
expect(scaffoldResults.files.length, 'expected scaffold files to be created').gt(0)
// expected scaffold files to be created
expect(scaffoldResults.files.length).toBeGreaterThan(0)
const specs = addTemplatesAsSpecs(scaffoldResults)
const checkAfterScaffolding = await hasNonExampleSpec(templates.e2eExamples, specs)
expect(checkAfterScaffolding, 'expected only having template files to show no non-example specs').to.be.false
// expected only having template files to show no non-example specs
expect(checkAfterScaffolding).toBe(false)
const fileName = 'my-test-file.js'
const scaffoldTemplateAction: Action = {
@@ -411,30 +415,31 @@ describe('code-generator', () => {
const checkAfterTemplate = await hasNonExampleSpec(templates.e2eExamples, specsWithGenerated)
expect(checkAfterTemplate, 'expected check after adding a new spec to indicate there are now non-example specs').to.be.true
// expected check after adding a new spec to indicate there are now non-example specs
expect(checkAfterTemplate).toBe(true)
})
it('should error if template dir does not exist', async () => {
it('should error if template dir does not exist', () => {
const singleSpec = ['sample.spec.ts']
expect(async () => await hasNonExampleSpec('', singleSpec)).to.throw
expect(hasNonExampleSpec('', singleSpec)).rejects.toThrow()
})
})
context('hasNonExampleSpec', async () => {
describe('hasNonExampleSpec', () => {
it('should error if template dir does not exist', () => {
expect(async () => await getExampleSpecPaths('')).to.throw
expect(getExampleSpecPaths('')).rejects.toThrow()
})
it('should return relative paths to example specs', async () => {
const results = await getExampleSpecPaths(templates.e2eExamples)
expect(results.length).to.be.greaterThan(0)
expect(results.length).toBeGreaterThan(0)
results.forEach((specPath) => {
const fullPathToSpec = path.join(templates.e2eExamples, specPath)
expect(fs.pathExistsSync(fullPathToSpec), `expected to find file at ${fullPathToSpec}`).to.be.true
expect(fs.pathExistsSync(fullPathToSpec)).toBe(true)
})
})
})
@@ -1,9 +1,8 @@
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import { defaultSpecPattern } from '@packages/config'
import { CT_FRAMEWORKS } from '@packages/scaffold-config'
import { expect } from 'chai'
import fs from 'fs-extra'
import path from 'path'
import sinon from 'sinon'
import { DataContext } from '../../../src'
import { SpecOptions, expectedSpecExtensions } from '../../../src/codegen/spec-options'
import { createTestDataContext } from '../helper'
@@ -25,10 +24,10 @@ describe('spec-options', () => {
describe('getCodeGenOptions', () => {
it('uses expected set of spec extensions', () => {
expect(expectedSpecExtensions).to.deep.eq(['.cy', '.spec', '.test', '-spec', '-test', '_spec'])
expect(expectedSpecExtensions).toEqual(['.cy', '.spec', '.test', '-spec', '-test', '_spec'])
})
context('unique file names', () => {
describe('unique file names', () => {
for (const specExtension of expectedSpecExtensions) {
it(`generates options for names with extension ${specExtension}`, async () => {
const testSpecOptions = new SpecOptions({
@@ -41,8 +40,8 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName${specExtension}.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName${specExtension}.js`)
})
}
@@ -57,8 +56,8 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName.js`)
})
it('generates options for file name with multiple extensions', async () => {
@@ -72,61 +71,60 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName.foo.bar.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName.foo.bar.js`)
})
})
context('create from component', () => {
afterEach(function () {
sinon.restore()
})
context('Vue', () => {
describe('create from component', () => {
describe('Vue', () => {
it('generates options for generating a Vue component spec', async () => {
const testSpecOptions = new SpecOptions({
currentProject: 'path/to/myProject',
codeGenPath: `${tmpPath}/MyComponent.vue`,
codeGenType: 'component',
isDefaultSpecPattern: true,
// @ts-expect-error
framework: CT_FRAMEWORKS[1],
specPattern: [defaultSpecPattern.component],
})
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('vueComponent')
expect(result.fileName).to.eq('MyComponent.cy.js')
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('vueComponent')
expect(result.fileName).toEqual('MyComponent.cy.js')
})
it('creates copy file if spec already exists', async () => {
sinon.stub(fs, 'access').onFirstCall().resolves().onSecondCall().rejects()
jest.spyOn(fs, 'access').mockImplementationOnce(() => Promise.resolve()).mockImplementationOnce(() => Promise.reject())
const testSpecOptions = new SpecOptions({
currentProject: 'path/to/myProject',
codeGenPath: `${tmpPath}/MyComponent.vue`,
codeGenType: 'component',
isDefaultSpecPattern: true,
// @ts-expect-error
framework: CT_FRAMEWORKS[1],
specPattern: [defaultSpecPattern.component],
})
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('vueComponent')
expect(result.fileName).to.eq('MyComponent-copy-1.cy.js')
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('vueComponent')
expect(result.fileName).toEqual('MyComponent-copy-1.cy.js')
})
})
context('React', () => {
describe('React', () => {
it('generates options for generating a React component spec', async () => {
const testSpecOptions = new SpecOptions({
currentProject: 'path/to/myProject',
codeGenPath: `${tmpPath}/Counter.tsx`,
codeGenType: 'component',
isDefaultSpecPattern: true,
// @ts-expect-error
framework: CT_FRAMEWORKS[2],
specPattern: [defaultSpecPattern.component],
componentName: 'Counter',
@@ -135,9 +133,9 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('reactComponent')
expect(result.fileName).to.eq('Counter.cy.tsx')
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('reactComponent')
expect(result.fileName).toEqual('Counter.cy.tsx')
})
it('creates a spec file with file and component names combined if they are different', async () => {
@@ -146,6 +144,7 @@ describe('spec-options', () => {
codeGenPath: `${tmpPath}/Counter.tsx`,
codeGenType: 'component',
isDefaultSpecPattern: true,
// @ts-expect-error
framework: CT_FRAMEWORKS[2],
specPattern: [defaultSpecPattern.component],
componentName: 'View',
@@ -153,19 +152,22 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('reactComponent')
expect(result.fileName).to.eq('CounterView.cy.tsx')
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('reactComponent')
expect(result.fileName).toEqual('CounterView.cy.tsx')
})
it('creates copy file if spec already exists', async () => {
sinon.stub(fs, 'access').onFirstCall().resolves().onSecondCall().rejects()
jest.spyOn(fs, 'access')
.mockImplementationOnce(() => Promise.resolve())
.mockImplementationOnce(() => Promise.reject())
const testSpecOptions = new SpecOptions({
currentProject: 'path/to/myProject',
codeGenPath: `${tmpPath}/Counter.tsx`,
codeGenType: 'component',
isDefaultSpecPattern: true,
// @ts-expect-error
framework: CT_FRAMEWORKS[2],
specPattern: [defaultSpecPattern.component],
componentName: 'View',
@@ -173,13 +175,13 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('reactComponent')
expect(result.fileName).to.eq('CounterView-copy-1.cy.tsx')
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('reactComponent')
expect(result.fileName).toEqual('CounterView-copy-1.cy.tsx')
})
})
context('custom spec pattern', () => {
describe('custom spec pattern', () => {
[{ testName: 'src/specs-folder/*.cy.{js,jsx}', componentPath: 'ComponentName.vue', specs: [], pattern: 'src/specs-folder/*.cy.{js,jsx}', expectedPath: 'src/specs-folder/ComponentName.cy.js' },
{ testName: 'src/**/*.{spec,cy}.{js,jsx,ts,tsx}', componentPath: 'MyComponent.vue', specs: [], pattern: 'src/**/*.{spec,cy}.{js,jsx,ts,tsx}', expectedPath: 'src/MyComponent.spec.ts', isTypescriptComponent: true },
{ testName: '**/*.test.js', componentPath: 'src/Foo.vue', specs: [], pattern: '**/*.test.js', expectedPath: 'cypress/Foo.test.js' },
@@ -195,13 +197,18 @@ describe('spec-options', () => {
it(testName, async () => {
// This stub simulates the spec file already existing the first time we try, which should cause a copy to be created
if (makeCopy) {
sinon.stub(fs, 'access').onFirstCall().resolves().onSecondCall().rejects()
jest.spyOn(fs, 'access')
.mockImplementationOnce(() => Promise.resolve())
.mockImplementationOnce(() => Promise.reject())
}
// This stub simulates that the component we are generating a spec from is using Typescript.
if (isTypescriptComponent) {
// @ts-ignore
sinon.stub(fs, 'readFile').resolves('lang="ts"')
jest.spyOn(fs, 'readFile').mockResolvedValue('lang="ts"')
} else {
// @ts-ignore
jest.spyOn(fs, 'readFile').mockResolvedValue('lang="js"')
}
const currentProject = 'path/to/myProject'
@@ -212,6 +219,7 @@ describe('spec-options', () => {
codeGenPath: `${tmpPath}/${componentPath}`,
codeGenType: 'component',
isDefaultSpecPattern: false,
// @ts-expect-error
framework: CT_FRAMEWORKS[1],
specPattern,
specs,
@@ -219,15 +227,15 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.templateKey).to.eq('vueComponent')
expect(`${result.overrideCodeGenDir}/${result.fileName}`).to.eq(expectedPath)
expect(result.codeGenType).toEqual('component')
expect(result.templateKey).toEqual('vueComponent')
expect(`${result.overrideCodeGenDir}/${result.fileName}`).toEqual(expectedPath)
})
})
})
})
context('duplicate files names', () => {
describe('duplicate files names', () => {
for (const specExtension of expectedSpecExtensions) {
it(`generates options for file name with extension ${specExtension}`, async () => {
const testSpecOptions = new SpecOptions({
@@ -242,16 +250,16 @@ describe('spec-options', () => {
let result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName-copy-1${specExtension}.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName-copy-1${specExtension}.js`)
// Add copy to file system and generate options again, index should increment
await fs.outputFile(`${tmpPath}/TestName-copy-1${specExtension}.js`, '// foo')
result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName-copy-2${specExtension}.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName-copy-2${specExtension}.js`)
})
}
@@ -268,16 +276,16 @@ describe('spec-options', () => {
let result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName-copy-1.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName-copy-1.js`)
// Add copy to file system and generate options again, index should increment
await fs.outputFile(`${tmpPath}/TestName-copy-1.js`, '// foo')
result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName-copy-2.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName-copy-2.js`)
})
it('generates options for file name with multiple extensions', async () => {
@@ -293,19 +301,19 @@ describe('spec-options', () => {
let result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName.foo.bar-copy-1.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName.foo.bar-copy-1.js`)
// Add copy to file system and generate options again, index should increment
await fs.outputFile(`${tmpPath}/TestName.foo.bar-copy-1.js`, '// foo')
result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName.foo.bar-copy-2.js`)
expect(result.codeGenType).toEqual('e2e')
expect(result.fileName).toEqual(`TestName.foo.bar-copy-2.js`)
})
context('file name contains special characters', async () => {
describe('file name contains special characters', () => {
[
{ condition: 'braces', fileName: '[...MyComponent].vue', expectedFileName: '[...MyComponent].cy.js', expectedComponentName: 'MyComponent' },
{ condition: 'hyphens', fileName: 'my-component.vue', expectedFileName: 'my-component.cy.js', expectedComponentName: 'MyComponent' },
@@ -322,6 +330,7 @@ describe('spec-options', () => {
codeGenType: 'component',
isDefaultSpecPattern: true,
specPattern: [defaultSpecPattern.component],
// @ts-expect-error
framework: CT_FRAMEWORKS[1],
})
@@ -329,9 +338,9 @@ describe('spec-options', () => {
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.fileName).to.eq(expectedFileName)
expect(result['componentName']).to.eq(expectedComponentName)
expect(result.codeGenType).toEqual('component')
expect(result.fileName).toEqual(expectedFileName)
expect(result['componentName']).toEqual(expectedComponentName)
})
})
})
@@ -1,4 +1,4 @@
import { expect } from 'chai'
import { describe, expect, it } from '@jest/globals'
import { stripIndent } from 'common-tags'
import { insertValueInJSString } from '../../src/util/config-file-updater'
@@ -13,7 +13,7 @@ const errors = {
}
describe('lib/util/config-file-updater', () => {
context('with js files', () => {
describe('with js files', () => {
describe('#insertValueInJSString', () => {
describe('es6 vs es5', () => {
it('finds the object literal and adds the values to it es6', async () => {
@@ -33,7 +33,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('finds the object literal and adds the values to it es5', async () => {
@@ -53,7 +53,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('finds the object literal by string literal and updates the values (es5)', async () => {
@@ -67,7 +67,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234' }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('works with and without the quotes around keys', async () => {
@@ -87,7 +87,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
})
@@ -111,7 +111,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('skips defineConfig even if it renamed in an import (es6)', async () => {
@@ -133,7 +133,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('skips defineConfig even if it renamed in a require (es5)', async () => {
@@ -155,7 +155,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { projectId: 'id1234', viewportWidth: 400 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
})
@@ -174,7 +174,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { foo: 1000 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('accepts inline comments', async () => {
@@ -193,7 +193,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { foo: 1000 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('updates a value even when this value is explicitely undefined', async () => {
@@ -212,7 +212,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { foo: 1000 }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('updates values and inserts config', async () => {
@@ -243,7 +243,7 @@ describe('lib/util/config-file-updater', () => {
const output = await insertValueInJSString(src, { foo: 1000, bar: 3000, projectId: 'id1234' }, errors)
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
})
@@ -266,7 +266,7 @@ describe('lib/util/config-file-updater', () => {
}
`
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('inserts nested values into existing keys', async () => {
@@ -291,7 +291,7 @@ describe('lib/util/config-file-updater', () => {
}
`
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
it('updates nested values', async () => {
@@ -315,27 +315,27 @@ describe('lib/util/config-file-updater', () => {
}
}`
expect(output).to.equal(expectedOutput)
expect(output).toEqual(expectedOutput)
})
})
describe('failures', () => {
it('fails if not an object literal', () => {
it('fails if not an object literal', async () => {
const src = [
'const foo = {}',
'export default foo',
].join('\n')
return insertValueInJSString(src, { bar: 10 }, errors)
.then(() => {
try {
await insertValueInJSString(src, { bar: 10 }, errors)
throw Error('this should not succeed')
})
.catch((err) => {
expect(err).to.have.property('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
})
} catch (err) {
expect(err).toHaveProperty('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
}
})
it('fails if one of the values to update is not a literal', () => {
it('fails if one of the values to update is not a literal', async () => {
const src = [
'const bar = 12',
'export default {',
@@ -343,16 +343,16 @@ describe('lib/util/config-file-updater', () => {
'}',
].join('\n')
return insertValueInJSString(src, { foo: 10 }, errors)
.then(() => {
try {
await insertValueInJSString(src, { foo: 10 }, errors)
throw Error('this should not succeed')
})
.catch((err) => {
expect(err).to.have.property('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
})
} catch (err) {
expect(err).toHaveProperty('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
}
})
it('fails with inlined values', () => {
it('fails with inlined values', async () => {
const src = stripIndent`\
const foo = 12
export default {
@@ -360,16 +360,16 @@ describe('lib/util/config-file-updater', () => {
}
`
return insertValueInJSString(src, { foo: 10 }, errors)
.then(() => {
try {
await insertValueInJSString(src, { foo: 10 }, errors)
throw Error('this should not succeed')
})
.catch((err) => {
expect(err).to.have.property('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
})
} catch (err) {
expect(err).toHaveProperty('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
}
})
it('fails if there is a spread', () => {
it('fails if there is a spread', async () => {
const src = stripIndent`\
const foo = { bar: 12 }
export default {
@@ -378,13 +378,13 @@ describe('lib/util/config-file-updater', () => {
}
`
return insertValueInJSString(src, { bar: 10 }, errors)
.then(() => {
try {
await insertValueInJSString(src, { bar: 10 }, errors)
throw Error('this should not succeed')
})
.catch((err) => {
expect(err).to.have.property('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
})
} catch (err) {
expect(err).toHaveProperty('type', 'COULD_NOT_UPDATE_CONFIG_FILE')
}
})
})
})
@@ -1,17 +1,29 @@
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import childProcess from 'child_process'
import { expect } from 'chai'
import semver from 'semver'
import sinon from 'sinon'
import { scaffoldMigrationProject as scaffoldProject } from '../helper'
import { ProjectConfigIpc } from '../../../src/data/ProjectConfigIpc'
jest.mock('child_process')
describe('ProjectConfigIpc', () => {
context('#eventProcessPid', () => {
describe('#eventProcessPid', () => {
let projectConfigIpc
beforeEach(async () => {
const projectPath = await scaffoldProject('e2e')
// @ts-expect-error - mock
childProcess.fork.mockImplementation(() => {
return {
on: jest.fn(),
once: jest.fn(),
emit: jest.fn(),
kill: jest.fn(),
removeAllListeners: jest.fn(),
}
})
projectConfigIpc = new ProjectConfigIpc(
undefined,
undefined,
@@ -26,16 +38,17 @@ describe('ProjectConfigIpc', () => {
afterEach(() => {
projectConfigIpc.cleanupIpc()
jest.clearAllMocks()
})
it('returns id for child process', () => {
const expectedId = projectConfigIpc._childProcess.pid
expect(projectConfigIpc.childProcessPid).to.eq(expectedId)
expect(projectConfigIpc.childProcessPid).toEqual(expectedId)
})
})
context('forkChildProcess', () => {
describe('forkChildProcess', () => {
// some of these node versions may not exist, but we want to verify
// the experimental flags are correctly disabled for future versions
const NODE_VERSIONS = ['20.5.1', '20.6.0', '20.19.1', '22.15.0']
@@ -43,26 +56,28 @@ describe('ProjectConfigIpc', () => {
const lastVersionWithDeprecatedLoaderOption = '20.5.1'
let projectConfigIpc
let forkSpy
beforeEach(() => {
process.env.CYPRESS_INTERNAL_MOCK_TYPESCRIPT_INSTALL = 'true'
forkSpy = sinon.spy(childProcess, 'fork')
})
afterEach(() => {
delete process.env.CYPRESS_INTERNAL_MOCK_TYPESCRIPT_INSTALL
forkSpy.restore()
projectConfigIpc.cleanupIpc()
})
context('typescript', () => {
describe('typescript', () => {
[...NODE_VERSIONS].forEach((nodeVersion) => {
const MOCK_NODE_PATH = `/Users/foo/.nvm/versions/node/v${nodeVersion}/bin/node`
const MOCK_NODE_VERSION = nodeVersion
context(`node v${nodeVersion}`, () => {
const PROJECTS = ['config-cjs-and-esm/config-with-ts-module', 'config-cjs-and-esm/config-with-module-resolution-bundler', 'config-cjs-and-esm/config-with-js-module', 'config-cjs-and-esm/config-with-cjs']
describe(`node v${nodeVersion}`, () => {
const PROJECTS = [
'config-cjs-and-esm/config-with-ts-module',
'config-cjs-and-esm/config-with-module-resolution-bundler',
'config-cjs-and-esm/config-with-js-module',
'config-cjs-and-esm/config-with-cjs',
]
PROJECTS.forEach((project) => {
it(`${project}: tsx generic loader (esm/commonjs/typescript)`, async () => {
@@ -82,36 +97,36 @@ describe('ProjectConfigIpc', () => {
// make sure that we use tsx for every file, regardless of typescript, esm, or commonjs
if (semver.lte(nodeVersion, lastVersionWithDeprecatedLoaderOption)) {
// For node 20.5.1 and down, we need use the --loader flag
expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
NODE_OPTIONS: sinon.match(/--loader ".*cypress\/node_modules\/tsx\/dist\/loader.mjs"/),
},
expect(childProcess.fork).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
env: expect.objectContaining({
NODE_OPTIONS: expect.stringMatching(/--loader ".*cypress\/node_modules\/tsx\/dist\/loader.mjs"/),
}),
}))
} else {
// For node 20.6.0 and up, we need use the --import flag
expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
NODE_OPTIONS: sinon.match(/--import ".*cypress\/node_modules\/tsx\/dist\/loader.mjs"/),
},
expect(childProcess.fork).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
env: expect.objectContaining({
NODE_OPTIONS: expect.stringMatching(/--import ".*cypress\/node_modules\/tsx\/dist\/loader.mjs"/),
}),
}))
}
if (project.includes('config-with-ts-module') || project.includes('config-with-module-resolution-bundler')) {
// these projects have typescript installed and have a tsconfig, so the TSX_TSCONFIG_PATH should be set to the project path
expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
TSX_TSCONFIG_PATH: sinon.match(`/cy-projects/${project}/tsconfig.json`),
},
expect(childProcess.fork).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
env: expect.objectContaining({
TSX_TSCONFIG_PATH: expect.stringMatching(`/cy-projects/${project}/tsconfig.json`),
}),
}))
} else {
// non typescript projects that do NOT have a tsconfig, so the TSX_TSCONFIG_PATH should be undefined
expect(forkSpy).to.have.been.calledWith(sinon.match.string, sinon.match.array, sinon.match({
env: {
TSX_TSCONFIG_PATH: undefined,
},
expect(childProcess.fork).toHaveBeenCalledWith(expect.any(String), expect.any(Array), expect.objectContaining({
env: expect.not.objectContaining({
TSX_TSCONFIG_PATH: expect.any(String),
}),
}))
}
})
}, 30000)
})
})
})
@@ -1,4 +1,4 @@
import { expect } from 'chai'
import { describe, expect, it, beforeEach } from '@jest/globals'
import { createTestDataContext } from '../helper'
import { ProjectConfigManager } from '../../../src/data/ProjectConfigManager'
import { EventRegistrar } from '../../../src/data/EventRegistrar'
@@ -23,20 +23,20 @@ describe('ProjectConfigManager', () => {
})
})
context('#eventProcessPid', () => {
describe('#eventProcessPid', () => {
it('returns process id from events ipc', () => {
// @ts-expect-error
configManager._eventsIpc = {
childProcessPid: 45699,
}
expect(configManager.eventProcessPid).to.eq(45699)
expect(configManager.eventProcessPid).toEqual(45699)
})
it('does not throw if config manager is not initialized', () => {
// @ts-expect-error
configManager._eventsIpc = undefined
expect(configManager.eventProcessPid).to.eq(undefined)
expect(configManager.eventProcessPid).toEqual(undefined)
})
})
})
@@ -1,14 +1,14 @@
import { expect } from 'chai'
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import type { DataContext } from '../../../src'
import path from 'path'
import { createTestDataContext } from '../helper'
import sinon from 'sinon'
import { FoundBrowser, FullConfig } from '@packages/types'
const browsers = [
{ name: 'electron', family: 'chromium', channel: 'stable', displayName: 'Electron' },
{ name: 'chrome', family: 'chromium', channel: 'stable', displayName: 'Chrome' },
{ name: 'chrome', family: 'chromium', channel: 'beta', displayName: 'Chrome Beta' },
{ name: 'firefox', family: 'firefox', channel: 'stable', displayName: 'Firefox' },
{ name: 'electron', family: 'chromium', channel: 'stable', displayName: 'Electron', path: '', version: '' },
{ name: 'chrome', family: 'chromium', channel: 'stable', displayName: 'Chrome', path: '', version: '' },
{ name: 'chrome', family: 'chromium', channel: 'beta', displayName: 'Chrome Beta', path: '', version: '' },
{ name: 'firefox', family: 'firefox', channel: 'stable', displayName: 'Firefox', path: '', version: '' },
]
let ctx: DataContext
@@ -16,10 +16,10 @@ let ctx: DataContext
function createDataContext (modeOptions?: Parameters<typeof createTestDataContext>[1]) {
const context = createTestDataContext('open', modeOptions)
context._apis.browserApi.getBrowsers = sinon.stub().resolves(browsers)
context._apis.projectApi.insertProjectPreferencesToCache = sinon.stub()
context.actions.project.launchProject = sinon.stub().resolves()
context.project.getProjectPreferences = sinon.stub().resolves(null)
jest.spyOn(context._apis.browserApi, 'getBrowsers').mockResolvedValue(browsers)
context._apis.projectApi.insertProjectPreferencesToCache = jest.fn()
jest.spyOn(context.actions.project, 'launchProject').mockResolvedValue(undefined)
jest.spyOn(context.project, 'getProjectPreferences').mockResolvedValue(null)
// @ts-expect-error
context.lifecycleManager._projectRoot = 'foo'
@@ -35,31 +35,42 @@ const fullConfig: FullConfig = {
describe('ProjectLifecycleManager', () => {
beforeEach(() => {
ctx = createDataContext()
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
})
context('#setInitialActiveBrowser', () => {
afterEach(() => {
// reset the working directory to the root of @packages/data-context
process.chdir(path.join(__dirname, '../../../'))
})
describe('#setInitialActiveBrowser', () => {
it('falls back to browsers[0] if preferences and cliBrowser do not exist', async () => {
ctx.coreData.activeBrowser = null
ctx.coreData.cliBrowser = null
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
expect(ctx.actions.project.launchProject).to.not.be.called
expect(ctx.coreData.activeBrowser).toEqual(expect.objectContaining({ name: 'electron' }))
expect(ctx.actions.project.launchProject).not.toHaveBeenCalled()
})
it('uses cli --browser option if one is set', async () => {
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('electron').resolves(browsers[0])
jest.spyOn(ctx._apis.browserApi, 'ensureAndGetByNameOrPath').mockImplementation((name) => {
if (name === 'electron') {
return Promise.resolve(browsers[0])
}
throw new Error('Browser not found')
})
ctx.coreData.activeBrowser = null
ctx.coreData.cliBrowser = 'electron'
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.cliBrowser).to.eq('electron')
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
expect(ctx.actions.project.launchProject).to.not.be.called
expect(ctx.coreData.cliBrowser).toEqual('electron')
expect(ctx.coreData.activeBrowser).toEqual(expect.objectContaining({ name: 'electron' }))
expect(ctx.actions.project.launchProject).not.toHaveBeenCalled()
})
it('uses cli --browser option and launches project if `--project --testingType` were used', async () => {
@@ -68,38 +79,44 @@ describe('ProjectLifecycleManager', () => {
testingType: 'e2e',
})
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('electron').resolves(browsers[0])
jest.spyOn(ctx._apis.browserApi, 'ensureAndGetByNameOrPath').mockImplementation((name) => {
if (name === 'electron') {
return Promise.resolve(browsers[0])
}
throw new Error('Browser not found')
})
ctx.coreData.activeBrowser = null
ctx.coreData.cliBrowser = 'electron'
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.cliBrowser).to.eq('electron')
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
expect(ctx.actions.project.launchProject).to.be.calledOnce
expect(ctx.coreData.cliBrowser).toEqual('electron')
expect(ctx.coreData.activeBrowser).toEqual(expect.objectContaining({ name: 'electron' }))
expect(ctx.actions.project.launchProject).toHaveBeenCalledTimes(1)
})
it('uses lastBrowser if available', async () => {
ctx.project.getProjectPreferences = sinon.stub().resolves({ lastBrowser: { name: 'chrome', channel: 'beta' } })
jest.spyOn(ctx.project, 'getProjectPreferences').mockResolvedValue({ lastBrowser: { name: 'chrome', channel: 'beta' } })
ctx.coreData.activeBrowser = null
ctx.coreData.cliBrowser = null
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'chrome', displayName: 'Chrome Beta' })
expect(ctx.actions.project.launchProject).to.not.be.called
expect(ctx.coreData.activeBrowser).toEqual(expect.objectContaining({ name: 'chrome', displayName: 'Chrome Beta' }))
expect(ctx.actions.project.launchProject).not.toHaveBeenCalled()
})
it('falls back to browsers[0] if lastBrowser does not exist', async () => {
ctx.project.getProjectPreferences = sinon.stub().resolves({ lastBrowser: { name: 'chrome', channel: 'dev' } })
jest.spyOn(ctx.project, 'getProjectPreferences').mockResolvedValue({ lastBrowser: { name: 'chrome', channel: 'dev' } })
ctx.coreData.activeBrowser = null
ctx.coreData.cliBrowser = null
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.coreData.activeBrowser).to.include({ name: 'electron' })
expect(ctx.actions.project.launchProject).to.not.be.called
expect(ctx.coreData.activeBrowser).toEqual(expect.objectContaining({ name: 'electron' }))
expect(ctx.actions.project.launchProject).not.toHaveBeenCalled()
})
it('uses config defaultBrowser option if --browser is not given', async () => {
@@ -109,18 +126,25 @@ describe('ProjectLifecycleManager', () => {
isBrowserGivenByCli: false,
})
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('chrome').resolves(browsers[1])
sinon.stub(ctx.lifecycleManager, 'loadedFullConfig').get(() => ({ defaultBrowser: 'chrome' }))
jest.spyOn(ctx._apis.browserApi, 'ensureAndGetByNameOrPath').mockImplementation((name) => {
if (name === 'chrome') {
return Promise.resolve(browsers[1])
}
expect(ctx.modeOptions.browser).to.eq(undefined)
expect(ctx.coreData.cliBrowser).to.eq(null)
expect(ctx.coreData.activeBrowser).to.eq(null)
throw new Error('Browser not found')
})
jest.spyOn(ctx.lifecycleManager, 'loadedFullConfig', 'get').mockReturnValue({ defaultBrowser: 'chrome' } as unknown as FullConfig)
expect(ctx.modeOptions.browser).toEqual(undefined)
expect(ctx.coreData.cliBrowser).toEqual(null)
expect(ctx.coreData.activeBrowser).toEqual(null)
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.modeOptions.browser).to.eq('chrome')
expect(ctx.coreData.cliBrowser).to.eq('chrome')
expect(ctx.coreData.activeBrowser).to.eq(browsers[1])
expect(ctx.modeOptions.browser).toEqual('chrome')
expect(ctx.coreData.cliBrowser).toEqual('chrome')
expect(ctx.coreData.activeBrowser).toEqual(browsers[1])
})
it('doesn\'t use config defaultBrowser option if --browser is given', async () => {
@@ -131,19 +155,26 @@ describe('ProjectLifecycleManager', () => {
isBrowserGivenByCli: true,
})
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('firefox').resolves(browsers[3])
sinon.stub(ctx.lifecycleManager, 'loadedFullConfig').get(() => ({ defaultBrowser: 'chrome' }))
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
jest.spyOn(ctx._apis.browserApi, 'ensureAndGetByNameOrPath').mockImplementation((name) => {
if (name === 'firefox') {
return Promise.resolve(browsers[3])
}
expect(ctx.modeOptions.browser).to.eq('firefox')
expect(ctx.coreData.cliBrowser).to.eq('firefox')
expect(ctx.coreData.activeBrowser).to.eq(null)
throw new Error('Browser not found')
})
jest.spyOn(ctx.lifecycleManager, 'loadedFullConfig', 'get').mockReturnValue({ defaultBrowser: 'chrome' } as unknown as FullConfig)
expect(ctx.modeOptions.browser).toEqual('firefox')
expect(ctx.coreData.cliBrowser).toEqual('firefox')
expect(ctx.coreData.activeBrowser).toEqual(null)
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.modeOptions.browser).to.eq('firefox')
expect(ctx.coreData.cliBrowser).to.eq('firefox')
expect(ctx.coreData.activeBrowser).to.eq(browsers[3])
expect(ctx.modeOptions.browser).toEqual('firefox')
expect(ctx.coreData.cliBrowser).toEqual('firefox')
expect(ctx.coreData.activeBrowser).toEqual(browsers[3])
})
it('ignores the defaultBrowser if there is an active browser and updates the CLI browser to the active browser', async () => {
@@ -153,27 +184,34 @@ describe('ProjectLifecycleManager', () => {
isBrowserGivenByCli: false,
})
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
ctx._apis.browserApi.ensureAndGetByNameOrPath = sinon.stub().withArgs('chrome:beta').resolves(browsers[2])
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
jest.spyOn(ctx._apis.browserApi, 'ensureAndGetByNameOrPath').mockImplementation((name) => {
if (name === 'chrome:beta') {
return Promise.resolve(browsers[2])
}
throw new Error('Browser not found')
})
// the default browser will be ignored since we have an active browser
sinon.stub(ctx.lifecycleManager, 'loadedFullConfig').get(() => ({ defaultBrowser: 'firefox' }))
jest.spyOn(ctx.lifecycleManager, 'loadedFullConfig', 'get').mockReturnValue({ defaultBrowser: 'firefox' } as unknown as FullConfig)
// set the active browser to chrome:beta
ctx.actions.browser.setActiveBrowser(browsers[2] as FoundBrowser)
expect(ctx.modeOptions.browser).to.eq(undefined)
expect(ctx.coreData.cliBrowser).to.eq(null)
expect(ctx.coreData.activeBrowser).to.eq(browsers[2])
expect(ctx.modeOptions.browser).toEqual(undefined)
expect(ctx.coreData.cliBrowser).toBeNull()
expect(ctx.coreData.activeBrowser).toEqual(browsers[2])
await ctx.lifecycleManager.setInitialActiveBrowser()
expect(ctx.modeOptions.browser).to.eq('chrome:beta')
expect(ctx.coreData.cliBrowser).to.eq('chrome:beta')
expect(ctx.coreData.activeBrowser).to.eq(browsers[2])
expect(ctx.modeOptions.browser).toEqual('chrome:beta')
expect(ctx.coreData.cliBrowser).toEqual('chrome:beta')
expect(ctx.coreData.activeBrowser).toEqual(browsers[2])
})
})
context('#eventProcessPid', () => {
describe('#eventProcessPid', () => {
it('returns process id from config manager', () => {
// @ts-expect-error
ctx.lifecycleManager._configManager = {
@@ -181,13 +219,13 @@ describe('ProjectLifecycleManager', () => {
destroy: () => {},
}
expect(ctx.lifecycleManager.eventProcessPid).to.eq(12399)
expect(ctx.lifecycleManager.eventProcessPid).toEqual(12399)
})
it('does not throw if config manager is not initialized', () => {
// @ts-expect-error
ctx.lifecycleManager._configManager = undefined
expect(ctx.lifecycleManager.eventProcessPid).to.eq(undefined)
expect(ctx.lifecycleManager.eventProcessPid).toEqual(undefined)
})
})
})
+25 -22
View File
@@ -1,5 +1,5 @@
// necessary to have mocha types working correctly
import 'mocha'
import { jest } from '@jest/globals'
import path from 'path'
import fs from 'fs-extra'
import { Response } from 'cross-fetch'
@@ -9,14 +9,13 @@ import { graphqlSchema } from '../../graphql/schema'
import { remoteSchemaWrapped as schemaCloud } from '../../graphql/stitching/remoteSchemaWrapped'
import type { BrowserApiShape } from '../../src/sources/BrowserDataSource'
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape, CohortsApiShape } from '../../src/actions'
import sinon from 'sinon'
import { execute, parse } from 'graphql'
import { getOperationName } from '@urql/core'
import { CloudQuery } from '../../test/graphql/stubCloudTypes'
import { remoteSchema } from '../../graphql/stitching/remoteSchema'
import type { OpenModeOptions, RunModeOptions } from '@packages/types'
import { GET_MAJOR_VERSION_FOR_CONTENT } from '@packages/types'
import { RelevantRunInfo } from '../../src/gen/graphcache-config.gen'
import type { RelevantRunInfo } from '../../src/gen/graphcache-config.gen'
type SystemTestProject = typeof fixtureDirs[number]
type SystemTestProjectPath<T extends SystemTestProject> = `${string}/system-tests/projects/${T}`
@@ -47,38 +46,41 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run',
modeOptions,
appApi: {} as AppApiShape,
localSettingsApi: {
getPreferences: sinon.stub().resolves({
getPreferences: jest.fn().mockResolvedValue({
majorVersionWelcomeDismissed: { [GET_MAJOR_VERSION_FOR_CONTENT()]: 123456 },
notifyWhenRunCompletes: ['failed'],
}),
getAvailableEditors: sinon.stub(),
setPreferences: sinon.stub(),
getAvailableEditors: jest.fn(),
setPreferences: jest.fn(),
} as unknown as LocalSettingsApiShape,
authApi: {
logIn: sinon.stub().throws('not stubbed'),
resetAuthState: sinon.stub(),
logIn: jest.fn().mockImplementation(() => {
throw new Error('not stubbed')
}),
resetAuthState: jest.fn(),
} as unknown as AuthApiShape,
projectApi: {
closeActiveProject: sinon.stub(),
insertProjectToCache: sinon.stub().resolves(),
getProjectRootsFromCache: sinon.stub().resolves([]),
runSpec: sinon.stub(),
routeToDebug: sinon.stub(),
closeActiveProject: jest.fn(),
insertProjectToCache: jest.fn().mockResolvedValue(undefined),
getProjectRootsFromCache: jest.fn().mockResolvedValue([]),
runSpec: jest.fn(),
routeToDebug: jest.fn(),
} as unknown as ProjectApiShape,
electronApi: {
isMainWindowFocused: sinon.stub().returns(false),
focusMainWindow: sinon.stub(),
copyTextToClipboard: (text) => {},
isMainWindowFocused: jest.fn().mockReturnValue(false),
focusMainWindow: jest.fn(),
copyTextToClipboard: (text: string) => {},
} as unknown as ElectronApiShape,
browserApi: {
focusActiveBrowserWindow: sinon.stub(),
getBrowsers: sinon.stub().resolves([]),
ensureAndGetByNameOrPath: jest.fn(),
focusActiveBrowserWindow: jest.fn(),
getBrowsers: jest.fn().mockResolvedValue([]),
} as unknown as BrowserApiShape,
cohortsApi: {
getCohorts: sinon.stub().resolves(),
getCohort: sinon.stub().resolves(),
insertCohort: sinon.stub(),
determineCohort: sinon.stub().resolves(),
getCohorts: jest.fn().mockResolvedValue(undefined),
getCohort: jest.fn().mockResolvedValue(undefined),
insertCohort: jest.fn(),
determineCohort: jest.fn().mockResolvedValue(undefined),
} as unknown as CohortsApiShape,
})
@@ -115,6 +117,7 @@ export function createTestDataContext (mode: DataContextConfig['mode'] = 'run',
export function createRelevantRun (runNumber: number): RelevantRunInfo {
return {
runNumber,
// @ts-expect-error - ciBuildNumber is not in the type
ciBuildNumber: '123',
branch: 'feature/branch',
organizationId: 'org-id',
@@ -1,5 +1,4 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import { DataContext } from '../../../src'
import { Poller } from '../../../src/polling'
@@ -7,92 +6,94 @@ import { createTestDataContext } from '../helper'
describe('Poller', () => {
let ctx: DataContext
let clock: sinon.SinonFakeTimers
beforeEach(() => {
sinon.restore()
clock = sinon.useFakeTimers()
jest.useFakeTimers()
ctx = createTestDataContext('open')
})
afterEach(() => {
clock.restore()
jest.useRealTimers()
})
it('polls', async () => {
const callback = sinon.stub()
const callback = jest.fn()
const interval = 5
const poller = new Poller(ctx, 'relevantRunChange', interval, callback)
const iterator = poller.start()
expect(callback).to.have.been.calledOnce
expect(callback).toHaveBeenCalledTimes(1)
await clock.tickAsync(interval * 1000)
expect(callback).to.have.been.calledTwice
await jest.advanceTimersByTimeAsync(interval * 1000)
expect(callback).toHaveBeenCalledTimes(2)
//stop iterator
iterator.return(undefined)
await clock.tickAsync(interval * 1000)
expect(callback, 'should not be called again after iterator stopped').to.have.been.calledTwice
await jest.advanceTimersByTimeAsync(interval * 1000)
// should not be called again after iterator stopped
expect(callback).toHaveBeenCalledTimes(2)
})
it('can change interval', async () => {
const callback = sinon.stub()
const callback = jest.fn()
const interval = 5
const poller = new Poller(ctx, 'relevantRunChange', interval, callback)
const iterator = poller.start()
expect(callback).to.have.been.calledOnce
expect(callback).toHaveBeenCalledTimes(1)
await clock.tickAsync(interval * 1000)
expect(callback).to.have.been.calledTwice
await jest.advanceTimersByTimeAsync(interval * 1000)
expect(callback).toHaveBeenCalledTimes(2)
poller.interval = 10
await clock.tickAsync(interval * 1000)
expect(callback, 'should be called at one original interval after interval change').to.have.been.calledThrice
await jest.advanceTimersByTimeAsync(interval * 1000)
// should be called at one original interval after interval change'
expect(callback).toHaveBeenCalledTimes(3)
await clock.tickAsync(interval * 1000)
expect(callback, 'should not be called yet with longer interval').to.have.been.calledThrice
await jest.advanceTimersByTimeAsync(interval * 1000)
// should not be called yet with longer interval
expect(callback).toHaveBeenCalledTimes(3)
await clock.tickAsync(interval * 1000)
expect(callback, 'should be called after longer interval').to.have.callCount(4)
await jest.advanceTimersByTimeAsync(interval * 1000)
// should be called after longer interval
expect(callback).toHaveBeenCalledTimes(4)
//stop iterator
iterator.return(undefined)
})
it('handles multiple pollers for the same event', async () => {
const callback = sinon.stub()
const callback = jest.fn()
const interval = 5
const poller = new Poller(ctx, 'relevantRunChange', interval, callback)
const iterator1 = poller.start()
expect(callback).to.have.been.calledOnce
expect(callback).toHaveBeenCalledTimes(1)
await clock.tickAsync(interval * 1000)
expect(callback).to.have.been.calledTwice
await jest.advanceTimersByTimeAsync(interval * 1000)
expect(callback).toHaveBeenCalledTimes(2)
const iterator2 = poller.start()
await clock.tickAsync(interval * 1000)
expect(callback).to.have.been.calledThrice
await jest.advanceTimersByTimeAsync(interval * 1000)
expect(callback).toHaveBeenCalledTimes(3)
iterator1.return(undefined)
iterator2.return(undefined)
await clock.tickAsync(interval * 1000)
expect(callback).to.have.been.calledThrice
await jest.advanceTimersByTimeAsync(interval * 1000)
expect(callback).toHaveBeenCalledTimes(3)
})
it('returns initial value', async () => {
const callback = sinon.stub()
const callback = jest.fn()
const interval = 5
const initialValue = { foo: true }
@@ -100,38 +101,38 @@ describe('Poller', () => {
const poller = new Poller<any, any>(ctx, 'relevantRunChange', interval, callback)
const iterator1 = poller.start({ initialValue })
expect(callback).to.have.been.calledOnce
expect(callback).toHaveBeenCalledTimes(1)
const result1 = await iterator1.next()
expect(result1.value).to.eq(initialValue)
expect(result1.value).toEqual(initialValue)
})
it('stores and returns meta values for each subscription', async () => {
const callback = sinon.stub()
const callback = jest.fn()
const interval = 5
const poller = new Poller<'relevantRunChange', { name: string }, { name: string}>(ctx, 'relevantRunChange', interval, callback)
expect(poller.subscriptions).to.have.length(0)
expect(poller.subscriptions).toHaveLength(0)
const iterator1 = poller.start({ meta: { name: 'one' } })
expect(poller.subscriptions).to.have.length(1)
expect(poller.subscriptions.map((sub) => sub.meta.name)).to.eql(['one'])
expect(poller.subscriptions).toHaveLength(1)
expect(poller.subscriptions.map((sub) => sub.meta.name)).toEqual(['one'])
const iterator2 = poller.start({ meta: { name: 'two' } })
expect(poller.subscriptions).to.have.length(2)
expect(poller.subscriptions.map((sub) => sub.meta.name)).to.eql(['one', 'two'])
expect(poller.subscriptions).toHaveLength(2)
expect(poller.subscriptions.map((sub) => sub.meta.name)).toEqual(['one', 'two'])
iterator1.return(undefined)
expect(poller.subscriptions).to.have.length(1)
expect(poller.subscriptions.map((sub) => sub.meta.name)).to.eql(['two'])
expect(poller.subscriptions).toHaveLength(1)
expect(poller.subscriptions.map((sub) => sub.meta.name)).toEqual(['two'])
iterator2.return(undefined)
expect(poller.subscriptions).to.have.length(0)
expect(poller.subscriptions).toHaveLength(0)
})
})
@@ -1,13 +1,8 @@
import chai from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { describe, expect, it, jest } from '@jest/globals'
import { FullConfig } from '@packages/types'
import { createTestDataContext } from '../helper'
import { userBrowser, foundBrowserChrome } from '../../fixtures/browsers'
chai.use(sinonChai)
const { expect } = chai
describe('BrowserDataSource', () => {
describe('#allBrowsers', () => {
it('returns machine browser if no user custom browsers resolved in config', async () => {
@@ -18,12 +13,12 @@ describe('BrowserDataSource', () => {
const ctx = createTestDataContext('run')
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
ctx.coreData.machineBrowsers = Promise.resolve([foundBrowserChrome])
const result = await ctx.browser.allBrowsers()
expect(result).to.eql([foundBrowserChrome])
expect(result).toEqual([foundBrowserChrome])
})
it('populates coreData.allBrowsers is not populated', async () => {
@@ -34,13 +29,13 @@ describe('BrowserDataSource', () => {
const ctx = createTestDataContext('run')
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
ctx.coreData.machineBrowsers = Promise.resolve([foundBrowserChrome])
const result = await ctx.browser.allBrowsers()
expect(result.length).to.eq(2)
expect(result[1].custom).to.be.true
expect(result.length).toEqual(2)
expect(result[1].custom).toEqual(true)
})
it('does not add user custom browser if name and version matches a machine browser', async () => {
@@ -54,12 +49,12 @@ describe('BrowserDataSource', () => {
const ctx = createTestDataContext('run')
sinon.stub(ctx.lifecycleManager, 'getFullInitialConfig').resolves(fullConfig)
jest.spyOn(ctx.lifecycleManager, 'getFullInitialConfig').mockResolvedValue(fullConfig)
ctx.coreData.machineBrowsers = Promise.resolve([machineBrowser])
const result = await ctx.browser.allBrowsers()
expect(result).to.eql([machineBrowser])
expect(result).toEqual([machineBrowser])
})
it('returns coreData.allBrowsers if populated', async () => {
@@ -70,7 +65,7 @@ describe('BrowserDataSource', () => {
const result = await ctx.browser.allBrowsers()
expect(result).to.eql(allBrowsers)
expect(result).toEqual(allBrowsers)
})
})
})
@@ -1,12 +1,10 @@
import sinon from 'sinon'
import { describe, expect, it, jest } from '@jest/globals'
import { execute } from 'graphql'
import chaiAsPromised from 'chai-as-promised'
import { Response } from 'cross-fetch'
import { DataContext } from '../../../src/DataContext'
import { CloudDataResponse, CloudDataSource } from '../../../src/sources'
import { createTestDataContext, scaffoldProject } from '../helper'
import chai, { expect } from 'chai'
import { ExecutionResult } from '@urql/core'
import {
CLOUD_PROJECT_QUERY,
@@ -20,24 +18,22 @@ import {
FAKE_USER_WITH_REQUIRED_RESOLVED_RESPONSE,
} from './fixtures/graphqlFixtures'
chai.use(chaiAsPromised)
describe('CloudDataSource', () => {
let cloudDataSource: CloudDataSource
let fetchStub: sinon.SinonStub
let getUserStub: sinon.SinonStub
let logoutStub: sinon.SinonStub
let invalidateCacheStub: sinon.SinonStub
let fetchStub: jest.Mock<() => Promise<Response>>
let getUserStub: jest.Mock<() => { authToken: string } | null>
let logoutStub: jest.Mock<() => void>
let invalidateCacheStub: jest.Mock<() => void>
let ctx: DataContext
beforeEach(() => {
sinon.restore()
fetchStub = sinon.stub()
fetchStub.resolves(new Response(JSON.stringify(FAKE_USER_RESPONSE), { status: 200 }))
getUserStub = sinon.stub()
getUserStub.returns({ authToken: '1234' })
logoutStub = sinon.stub()
invalidateCacheStub = sinon.stub()
jest.restoreAllMocks()
fetchStub = jest.fn()
fetchStub.mockResolvedValue(new Response(JSON.stringify(FAKE_USER_RESPONSE), { status: 200 }))
getUserStub = jest.fn()
getUserStub.mockReturnValue({ authToken: '1234' })
logoutStub = jest.fn()
invalidateCacheStub = jest.fn()
ctx = createTestDataContext('open')
cloudDataSource = new CloudDataSource({
fetch: fetchStub,
@@ -53,7 +49,7 @@ describe('CloudDataSource', () => {
describe('excecuteRemoteGraphQL', () => {
it('returns immediately with { data: null } when no user is defined', () => {
getUserStub.returns(null)
getUserStub.mockReturnValue(null)
const result = cloudDataSource.executeRemoteGraphQL({
fieldName: 'cloudViewer',
operationDoc: FAKE_USER_QUERY,
@@ -61,8 +57,8 @@ describe('CloudDataSource', () => {
operationType: 'query',
})
expect(result).to.eql({ data: null })
expect(fetchStub).not.to.be.called
expect(result).toEqual({ data: null })
expect(fetchStub).not.toHaveBeenCalled()
})
it('issues a fetch request for the data when the user is defined', async () => {
@@ -75,7 +71,7 @@ describe('CloudDataSource', () => {
const resolved = await result
expect(resolved.data).to.eql(FAKE_USER_RESPONSE.data)
expect(resolved.data).toEqual(FAKE_USER_RESPONSE.data)
})
it('only issues a single fetch if the operation is called twice', async () => {
@@ -92,12 +88,12 @@ describe('CloudDataSource', () => {
operationType: 'query',
})
expect(result1).to.eq(result2)
expect(result1).toEqual(result2)
const resolved = await result1
expect(resolved.data).to.eql(FAKE_USER_RESPONSE.data)
expect(fetchStub).to.have.been.calledOnce
expect(resolved.data).toEqual(FAKE_USER_RESPONSE.data)
expect(fetchStub).toHaveBeenCalledTimes(1)
})
it('resolves eagerly with the cached data if the data has already been resolved', async () => {
@@ -117,8 +113,8 @@ describe('CloudDataSource', () => {
operationType: 'query',
})
expect((immediateResult as ExecutionResult).data).to.eql(FAKE_USER_RESPONSE.data)
expect(fetchStub).to.have.been.calledOnce
expect((immediateResult as ExecutionResult).data).toEqual(FAKE_USER_RESPONSE.data)
expect(fetchStub).toHaveBeenCalledTimes(1)
})
it('when there is a nullable field missing, resolves with the eager result & fetches for the rest', async () => {
@@ -131,7 +127,7 @@ describe('CloudDataSource', () => {
await result
fetchStub.resolves(new Response(JSON.stringify(FAKE_USER_WITH_OPTIONAL_RESOLVED_RESPONSE), { status: 200 }))
fetchStub.mockResolvedValue(new Response(JSON.stringify(FAKE_USER_WITH_OPTIONAL_RESOLVED_RESPONSE), { status: 200 }))
const immediateResult = cloudDataSource.executeRemoteGraphQL({
fieldName: 'cloudViewer',
@@ -140,14 +136,14 @@ describe('CloudDataSource', () => {
operationType: 'query',
})
expect((immediateResult as CloudDataResponse).data).to.eql(FAKE_USER_WITH_OPTIONAL_MISSING_RESPONSE.data)
expect((immediateResult as CloudDataResponse).stale).to.eql(true)
expect((immediateResult as CloudDataResponse).data).toEqual(FAKE_USER_WITH_OPTIONAL_MISSING_RESPONSE.data)
expect((immediateResult as CloudDataResponse).stale).toEqual(true)
const executingResponse = await (immediateResult as CloudDataResponse).executing
expect(executingResponse.data).to.eql(FAKE_USER_WITH_OPTIONAL_RESOLVED_RESPONSE.data)
expect(executingResponse.data).toEqual(FAKE_USER_WITH_OPTIONAL_RESOLVED_RESPONSE.data)
expect(fetchStub).to.have.been.calledTwice
expect(fetchStub).toHaveBeenCalledTimes(2)
})
it('when there is a non-nullable field missing, issues the remote query immediately', async () => {
@@ -160,7 +156,7 @@ describe('CloudDataSource', () => {
await result
fetchStub.resolves(new Response(JSON.stringify(FAKE_USER_WITH_REQUIRED_RESOLVED_RESPONSE), { status: 200 }))
fetchStub.mockResolvedValue(new Response(JSON.stringify(FAKE_USER_WITH_REQUIRED_RESOLVED_RESPONSE), { status: 200 }))
const requiredResult = cloudDataSource.executeRemoteGraphQL({
fieldName: 'cloudViewer',
@@ -169,15 +165,15 @@ describe('CloudDataSource', () => {
operationType: 'query',
})
expect(requiredResult).to.be.instanceOf(Promise)
expect(requiredResult).toBeInstanceOf(Promise)
expect((await requiredResult).data).to.eql(FAKE_USER_WITH_REQUIRED_RESOLVED_RESPONSE.data)
expect((await requiredResult).data).toEqual(FAKE_USER_WITH_REQUIRED_RESOLVED_RESPONSE.data)
expect(fetchStub).to.have.been.calledTwice
expect(fetchStub).toHaveBeenCalledTimes(2)
})
it('returns error property on response', async () => {
fetchStub.resolves(new Response(JSON.stringify(new Error('Unauthorized')), { status: 200 }))
fetchStub.mockResolvedValue(new Response(JSON.stringify(new Error('Unauthorized')), { status: 200 }))
const result = cloudDataSource.executeRemoteGraphQL({
fieldName: 'cloudViewer',
@@ -188,13 +184,13 @@ describe('CloudDataSource', () => {
const resolved = await result
expect(resolved.data).to.eql(undefined)
expect(resolved.errors).to.exist
expect(resolved.error?.networkError?.message).to.eql('No Content')
expect(resolved.data).toEqual(undefined)
expect(resolved.errors).toBeDefined()
expect(resolved.error?.networkError?.message).toEqual('No Content')
})
it('logout user on 401 response', async () => {
fetchStub.resolves(new Response(JSON.stringify(new Error('Unauthorized')), { status: 401 }))
fetchStub.mockResolvedValue(new Response(JSON.stringify(new Error('Unauthorized')), { status: 401 }))
const result = cloudDataSource.executeRemoteGraphQL({
fieldName: 'cloudViewer',
@@ -205,11 +201,11 @@ describe('CloudDataSource', () => {
const resolved = await result
expect(resolved.data).to.eql(undefined)
expect(resolved.errors).to.exist
expect(resolved.error?.networkError?.message).to.eql('Unauthorized')
expect(resolved.data).toEqual(undefined)
expect(resolved.errors).toBeDefined()
expect(resolved.error?.networkError?.message).toEqual('Unauthorized')
expect(logoutStub).to.have.been.calledOnce
expect(logoutStub).toHaveBeenCalledTimes(1)
})
})
@@ -220,7 +216,7 @@ describe('CloudDataSource', () => {
operationVariables: {},
})
expect(result).to.eql(false)
expect(result).toEqual(false)
})
it('returns true if we are currently resolving the request', () => {
@@ -236,7 +232,7 @@ describe('CloudDataSource', () => {
operationVariables: {},
})
expect(result).to.eql(true)
expect(result).toEqual(true)
})
})
@@ -247,7 +243,7 @@ describe('CloudDataSource', () => {
operationVariables: {},
})
expect(result).to.eql(false)
expect(result).toEqual(false)
})
it('returns true if we have resolved the data for the query', async () => {
@@ -263,7 +259,7 @@ describe('CloudDataSource', () => {
operationVariables: {},
})
expect(result).to.eql(true)
expect(result).toEqual(true)
})
})
@@ -279,24 +275,26 @@ describe('CloudDataSource', () => {
expect(cloudDataSource.hasResolved({
operationDoc: FAKE_USER_QUERY,
operationVariables: {},
})).to.eq(true)
})).toEqual(true)
await cloudDataSource.invalidate('Query', 'cloudViewer')
expect(cloudDataSource.hasResolved({
operationDoc: FAKE_USER_QUERY,
operationVariables: {},
})).to.eq(false)
})).toEqual(false)
})
})
describe('delegateCloudField', () => {
it('delegates a field to the remote schema, which calls executeRemoteGraphQL', async () => {
fetchStub.resolves(new Promise((resolve) => {
setTimeout(() => {
resolve(new Response(JSON.stringify(CLOUD_PROJECT_RESPONSE), { status: 200 }))
}, 200)
}))
fetchStub.mockImplementation(() => {
return new Promise<Response>((resolve) => {
setTimeout(() => {
resolve(new Response(JSON.stringify(CLOUD_PROJECT_RESPONSE), { status: 200 }))
}, 200)
})
})
Object.defineProperty(ctx, 'cloud', { value: cloudDataSource })
@@ -304,13 +302,13 @@ describe('CloudDataSource', () => {
const delegateCloudField = cloudDataSource.delegateCloudField
const delegateCloudSpy = sinon.stub(cloudDataSource, 'delegateCloudField').callsFake(async function (...args) {
const delegateCloudSpy = jest.spyOn(cloudDataSource, 'delegateCloudField').mockImplementation(async function (...args) {
return delegateCloudField.apply(this, args)
})
await ctx.actions.project.setCurrentProject(dir)
sinon.stub(ctx.project, 'projectId').resolves('abc1234')
jest.spyOn(ctx.project, 'projectId').mockResolvedValue('abc1234')
const result = await execute({
rootValue: {},
@@ -319,16 +317,16 @@ describe('CloudDataSource', () => {
contextValue: ctx,
})
expect(delegateCloudSpy).to.have.been.calledOnce
expect(delegateCloudSpy).toHaveBeenCalledTimes(1)
expect(result.data).to.eql({
expect(result.data).toEqual({
currentProject: {
cloudProject: null,
id: Buffer.from(`CurrentProject:${dir}`, 'utf8').toString('base64'),
},
})
expect(await delegateCloudSpy.firstCall.returnValue)
await new Promise((resolve) => setTimeout(resolve, 300))
const result2 = await execute({
rootValue: {},
@@ -337,7 +335,7 @@ describe('CloudDataSource', () => {
contextValue: ctx,
})
expect(result2.data).to.eql({
expect(result2.data).toEqual({
currentProject: {
cloudProject: {
__typename: 'CloudProject',
@@ -347,7 +345,7 @@ describe('CloudDataSource', () => {
},
})
expect(fetchStub).to.have.been.calledOnce
expect(fetchStub).toHaveBeenCalledTimes(1)
})
})
})
@@ -1,6 +1,5 @@
import sinon from 'sinon'
import { describe, expect, it, jest } from '@jest/globals'
import fs from 'fs-extra'
import { expect } from 'chai'
import path from 'path'
import os from 'os'
@@ -31,7 +30,6 @@ describe('FileDataSource', () => {
afterEach(() => {
removeProject('globby-test-bed')
sinon.restore()
})
describe('#getFilesByGlob', () => {
@@ -41,9 +39,9 @@ describe('FileDataSource', () => {
'root-script-*.js',
)
expect(files).to.have.length(2)
expect(files[0]).to.eq(path.join(projectPath, 'root-script-1.js'))
expect(files[1]).to.eq(path.join(projectPath, 'root-script-2.js'))
expect(files).toHaveLength(2)
expect(files[0]).toEqual(path.join(projectPath, 'root-script-1.js'))
expect(files[1]).toEqual(path.join(projectPath, 'root-script-2.js'))
})
it('finds files matching relative patterns in working dir', async () => {
@@ -52,7 +50,7 @@ describe('FileDataSource', () => {
'./root-script-*.js',
)
expect(files).to.have.length(2)
expect(files).toHaveLength(2)
})
it('finds files matching patterns that include working dir', async () => {
@@ -61,7 +59,7 @@ describe('FileDataSource', () => {
`${projectPath}/root-script-*.js`,
)
expect(files).to.have.length(2)
expect(files).toHaveLength(2)
})
it('does not replace working directory in glob pattern if it is not leading', async () => {
@@ -79,7 +77,7 @@ describe('FileDataSource', () => {
`./cypress${projectPath}/nested-script.js`,
)
expect(files).to.have.length(1)
expect(files).toHaveLength(1)
})
it('finds files matching multiple patterns', async () => {
@@ -88,7 +86,7 @@ describe('FileDataSource', () => {
['root-script-*.js', 'scripts/**/*.js'],
)
expect(files).to.have.length(5)
expect(files).toHaveLength(5)
})
it('does not find files outside of working dir', async () => {
@@ -97,7 +95,7 @@ describe('FileDataSource', () => {
['root-script-*.js', './**/*.js'],
)
expect(files).to.have.length(3)
expect(files).toHaveLength(3)
})
it('by default ignores files within node_modules', async () => {
@@ -115,7 +113,7 @@ describe('FileDataSource', () => {
// only scripts at root should be found, as node_modules is implicitly ignored
// and ./scripts is explicitly ignored
expect(files).to.have.length(2)
expect(files).toHaveLength(2)
})
it('does not ignores files within node_modules, if node_modules is in the glob path', async () => {
@@ -132,7 +130,7 @@ describe('FileDataSource', () => {
// scripts at root (2 of them) and scripts at node_modules should be found
// and ./scripts is explicitly ignored
expect(files).to.have.length(4)
expect(files).toHaveLength(4)
})
it('does not ignores files within node_modules, if node_modules is in the project path', async () => {
@@ -149,15 +147,18 @@ describe('FileDataSource', () => {
)
// only scripts at node_modules should be found, since it is the project path
expect(files).to.have.length(3)
expect(files).toHaveLength(3)
})
it('converts globs to POSIX paths on windows', async () => {
const windowsSeperator = '\\'
sinon.stub(os, 'platform').returns('win32')
const toPosixStub = sinon.stub(fileUtil, 'toPosix').callsFake((path) => {
return toPosixStub.wrappedMethod(path, windowsSeperator)
jest.spyOn(os, 'platform').mockReturnValue('win32')
const { toPosix: toPosixActual } = jest.requireActual<typeof import('../../../src/util/file')>('../../../src/util/file')
jest.spyOn(fileUtil, 'toPosix').mockImplementation((path) => {
return toPosixActual(path, windowsSeperator)
})
const files = await fileDataSource.getFilesByGlob(
@@ -165,7 +166,7 @@ describe('FileDataSource', () => {
`**${windowsSeperator}*script-*.js`,
)
expect(files).to.have.length(5)
expect(files).toHaveLength(5)
})
it('finds files using given globby options', async () => {
@@ -175,9 +176,9 @@ describe('FileDataSource', () => {
{ absolute: false },
)
expect(files).to.have.length(2)
expect(files[0]).to.eq('root-script-1.js')
expect(files[1]).to.eq('root-script-2.js')
expect(files).toHaveLength(2)
expect(files[0]).toEqual('root-script-1.js')
expect(files[1]).toEqual('root-script-2.js')
})
})
})
@@ -195,14 +196,8 @@ describe('FileDataSource', () => {
ignore: ['**/node_modules/**'],
}
let matchGlobsStub: sinon.SinonStub
beforeEach(() => {
matchGlobsStub = sinon.stub(FileDataSourceModule, 'matchGlobs').resolves(mockMatches)
})
afterEach(() => {
sinon.restore()
jest.spyOn(FileDataSourceModule, 'matchGlobs').mockResolvedValue(mockMatches)
})
it('matches absolute patterns when working directory is root', async () => {
@@ -211,8 +206,8 @@ describe('FileDataSource', () => {
'/cypress/e2e/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['cypress/e2e/**.cy.js'],
{ ...defaultGlobbyOptions, cwd: '/' },
)
@@ -224,8 +219,8 @@ describe('FileDataSource', () => {
'./project/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['./project/**.cy.js'],
{ ...defaultGlobbyOptions, cwd: '/' },
)
@@ -237,8 +232,8 @@ describe('FileDataSource', () => {
'project/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['project/**.cy.js'],
{ ...defaultGlobbyOptions, cwd: '/' },
)
@@ -250,8 +245,8 @@ describe('FileDataSource', () => {
'/my/project/cypress/e2e/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['cypress/e2e/**.cy.js'],
{ ...defaultGlobbyOptions, cwd: '/my/project' },
)
@@ -263,8 +258,8 @@ describe('FileDataSource', () => {
'/my/project/cypress/my/project/e2e/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['cypress/my/project/e2e/**.cy.js'],
{ ...defaultGlobbyOptions, cwd: '/my/project' },
)
@@ -277,8 +272,8 @@ describe('FileDataSource', () => {
{ ignore: ['ignore/foo.*', '/ignore/bar.*'] },
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['cypress/e2e/**.cy.js'],
{
...defaultGlobbyOptions,
@@ -294,8 +289,8 @@ describe('FileDataSource', () => {
'/cypress/e2e/**.cy.js',
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['/cypress/e2e/**.cy.js'],
{
...defaultGlobbyOptions,
@@ -314,8 +309,8 @@ describe('FileDataSource', () => {
],
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
[
'node_modules/cypress/e2e/**.cy.js',
'cypress/e2e/**.cy.js',
@@ -335,8 +330,8 @@ describe('FileDataSource', () => {
{ ignore: ['ignore/foo.*', '/ignore/bar.*'] },
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['/node_modules/test_package/e2e/**.cy.js'],
{
...defaultGlobbyOptions,
@@ -353,8 +348,8 @@ describe('FileDataSource', () => {
{ absolute: false, objectMode: true },
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.been.calledWith(
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledWith(
['cypress/e2e/**.cy.js'],
{
...defaultGlobbyOptions,
@@ -366,8 +361,13 @@ describe('FileDataSource', () => {
})
it('should retry search with `suppressErrors` if non-suppressed attempt fails', async () => {
matchGlobsStub.onFirstCall().rejects(new Error('mocked filesystem error'))
matchGlobsStub.onSecondCall().resolves(mockMatches)
jest.spyOn(FileDataSourceModule, 'matchGlobs')
.mockReset()
.mockImplementationOnce(() => {
return Promise.reject(new Error('mocked filesystem error'))
}).mockImplementationOnce(() => {
return Promise.resolve(mockMatches)
})
const files = await fileDataSource.getFilesByGlob(
'/',
@@ -375,14 +375,18 @@ describe('FileDataSource', () => {
{ absolute: false, objectMode: true },
)
expect(files).to.eq(mockMatches)
expect(matchGlobsStub).to.have.callCount(2)
expect(matchGlobsStub.getCall(0).args[1].suppressErrors).to.be.undefined
expect(matchGlobsStub.getCall(1).args[1].suppressErrors).to.equal(true)
expect(files).toEqual(mockMatches)
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledTimes(2)
expect(FileDataSourceModule.matchGlobs).toHaveBeenNthCalledWith(1, expect.any(Array), expect.not.objectContaining({ suppressErrors: expect.any(Boolean) }))
expect(FileDataSourceModule.matchGlobs).toHaveBeenNthCalledWith(2, expect.any(Array), expect.objectContaining({ suppressErrors: true }))
})
it('should return empty array if retry with suppression fails', async () => {
matchGlobsStub.rejects(new Error('mocked filesystem error'))
jest.spyOn(FileDataSourceModule, 'matchGlobs')
.mockReset()
.mockImplementation(() => {
return Promise.reject(new Error('mocked filesystem error'))
})
const files = await fileDataSource.getFilesByGlob(
'/',
@@ -390,8 +394,8 @@ describe('FileDataSource', () => {
{ absolute: false, objectMode: true },
)
expect(files).to.eql([])
expect(matchGlobsStub).to.have.callCount(2)
expect(files).toEqual([])
expect(FileDataSourceModule.matchGlobs).toHaveBeenCalledTimes(2)
})
})
})
@@ -1,9 +1,8 @@
import { assert, expect } from 'chai'
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import path from 'path'
import os from 'os'
import simpleGit from 'simple-git'
import fs from 'fs-extra'
import sinon from 'sinon'
import pDefer from 'p-defer'
import chokidar from 'chokidar'
@@ -42,12 +41,10 @@ describe('GitDataSource', () => {
}
gitInfo = undefined
sinon.restore()
})
it(`gets correct status for files on ${os.platform()}`, async function () {
const onBranchChange = sinon.stub()
const onBranchChange = jest.fn()
const dfd = pDefer()
// create a file and modify a file to express all
@@ -73,29 +70,29 @@ describe('GitDataSource', () => {
const gitInfoChangeResolve = await dfd.promise
expect(gitInfoChangeResolve).to.eql([fooSpec, aRecordSpec, xhrSpec])
expect(gitInfoChangeResolve).toEqual([fooSpec, aRecordSpec, xhrSpec])
const created = gitInfo.gitInfoFor(fooSpec)!
const unmodified = gitInfo.gitInfoFor(aRecordSpec)!
const modified = gitInfo.gitInfoFor(xhrSpec)!
expect(created.lastModifiedHumanReadable).to.match(/(a few|[0-9]) seconds? ago/)
expect(created.statusType).to.eql('created')
expect(created.lastModifiedHumanReadable).toMatch(/(a few|[0-9]) seconds? ago/)
expect(created.statusType).toEqual('created')
// do not want to set this explicitly in the test, since it can mess up your local git instance
expect(created.author).not.to.be.undefined
expect(created.lastModifiedTimestamp).not.to.be.undefined
expect(created.author).not.toBeUndefined()
expect(created.lastModifiedTimestamp).not.toBeUndefined()
expect(unmodified.lastModifiedHumanReadable).to.match(/(a few|[0-9]) seconds? ago/)
expect(unmodified.statusType).to.eql('unmodified')
expect(unmodified.lastModifiedHumanReadable).toMatch(/(a few|[0-9]) seconds? ago/)
expect(unmodified.statusType).toEqual('unmodified')
// do not want to set this explicitly in the test, since it can mess up your local git instance
expect(unmodified.author).not.to.be.undefined
expect(unmodified.lastModifiedTimestamp).not.to.be.undefined
expect(unmodified.author).not.toBeUndefined()
expect(unmodified.lastModifiedTimestamp).not.toBeUndefined()
expect(modified.lastModifiedHumanReadable).to.match(/(a few|[0-9]) seconds? ago/)
expect(modified.statusType).to.eql('modified')
expect(modified.lastModifiedHumanReadable).toMatch(/(a few|[0-9]) seconds? ago/)
expect(modified.statusType).toEqual('modified')
// do not want to set this explicitly in the test, since it can mess up your local git instance
expect(modified.author).not.to.be.undefined
expect(modified.lastModifiedTimestamp).not.to.be.undefined
expect(modified.author).not.toBeUndefined()
expect(modified.lastModifiedTimestamp).not.toBeUndefined()
})
it(`handles files with special characters on ${os.platform()}`, async () => {
@@ -128,9 +125,9 @@ describe('GitDataSource', () => {
gitInfo = new GitDataSource({
isRunMode: false,
projectRoot: projectPath,
onBranchChange: sinon.stub(),
onBranchChange: jest.fn(),
onGitInfoChange: dfd.resolve,
onError: sinon.stub(),
onError: jest.fn(),
})
await Promise.all(
@@ -145,44 +142,44 @@ describe('GitDataSource', () => {
return gitInfo.gitInfoFor(filepath)
})
expect(results).to.have.lengthOf(filepaths.length)
expect(results).toHaveLength(filepaths.length)
filepaths.forEach((filepath, index) => {
const result = results[index]
expect(result?.lastModifiedHumanReadable).to.match(/(a few|[0-9]) seconds? ago/)
expect(result?.statusType).to.eql('created')
expect(result?.lastModifiedHumanReadable).toMatch(/(a few|[0-9]) seconds? ago/)
expect(result?.statusType).toEqual('created')
})
})
it(`watches switching branches on ${os.platform()}`, async () => {
const stub = sinon.stub()
const stub = jest.fn()
const dfd = pDefer()
stub.onFirstCall().callsFake(dfd.resolve)
stub.mockImplementationOnce(dfd.resolve)
gitInfo = new GitDataSource({
isRunMode: false,
projectRoot: projectPath,
onBranchChange: stub,
onGitInfoChange: sinon.stub(),
onError: sinon.stub(),
onGitInfoChange: jest.fn(),
onError: jest.fn(),
})
const result = await dfd.promise
expect(result).to.eq((await git.branch()).current)
expect(result).toEqual((await git.branch()).current)
const switchBranch = pDefer()
stub.onSecondCall().callsFake(switchBranch.resolve)
stub.mockImplementationOnce(switchBranch.resolve)
git.checkoutLocalBranch('testing123')
expect(await switchBranch.promise).to.eq('testing123')
expect(await switchBranch.promise).toEqual('testing123')
})
it(`handles error while watching .git on ${os.platform()}`, async () => {
sinon.stub(chokidar, 'watch').callsFake(() => {
jest.spyOn(chokidar, 'watch').mockImplementation(() => {
const mockWatcher = {
on: (event, fn) => {
if (event === 'error') {
@@ -195,34 +192,34 @@ describe('GitDataSource', () => {
return mockWatcher as chokidar.FSWatcher
})
const errorStub = sinon.stub()
const stub = sinon.stub()
const errorStub = jest.fn()
const stub = jest.fn()
const dfd = pDefer()
stub.onFirstCall().callsFake(dfd.resolve)
stub.mockImplementationOnce(dfd.resolve)
gitInfo = new GitDataSource({
isRunMode: false,
projectRoot: projectPath,
onBranchChange: stub,
onGitInfoChange: sinon.stub(),
onGitInfoChange: jest.fn(),
onError: errorStub,
})
const result = await dfd.promise
expect(result).to.eq((await git.branch()).current)
expect(result).toEqual((await git.branch()).current)
expect(errorStub).to.be.callCount(1)
expect(errorStub).toHaveBeenCalledTimes(1)
})
context('Git Hashes - no fake timers', () => {
describe('Git Hashes - no fake timers', () => {
it('does not include commits that are part of the Git tree from a merge', async () => {
const dfd = pDefer()
const logCallback = sinon.stub()
const logCallback = jest.fn()
logCallback.onFirstCall().callsFake(dfd.resolve)
logCallback.mockImplementationOnce(dfd.resolve)
const mainBranch = (await git.branch()).current
@@ -254,16 +251,16 @@ describe('GitDataSource', () => {
gitInfo = new GitDataSource({
isRunMode: false,
projectRoot: projectPath,
onBranchChange: sinon.stub(),
onGitInfoChange: sinon.stub(),
onError: sinon.stub(),
onBranchChange: jest.fn(),
onGitInfoChange: jest.fn(),
onError: jest.fn(),
onGitLogChange: logCallback,
})
await dfd.promise
expect(gitInfo.currentHashes).to.have.length(3)
expect(gitInfo.currentHashes).not.to.contain(hashFromMerge)
expect(gitInfo.currentHashes).toHaveLength(3)
expect(gitInfo.currentHashes).not.toContain(hashFromMerge)
})
})
})
@@ -1,64 +1,52 @@
import { expect, use } from 'chai'
import sinonChai from 'sinon-chai'
import sinon from 'sinon'
import proxyquire from 'proxyquire'
import { describe, expect, it, jest } from '@jest/globals'
import pDefer, { DeferredPromise } from 'p-defer'
import EventEmitter from 'events'
import { SimpleGit } from 'simple-git'
import type { GitDataSource, GitDataSourceConfig } from '../../../src/sources/GitDataSource'
import type { SimpleGit } from 'simple-git'
import { GitDataSource } from '../../../src/sources/GitDataSource'
import Chokidar from 'chokidar'
use(sinonChai)
const stubbedSimpleGit: {
// Parameters<> only gets the last overload defined, which is
// supposed to be the most permissive. However, SimpleGit defines
// overloads in the opposite order, and we need the one that takes
// a string.
revparse: jest.Mock<(option: string) => R<'revparse'>>
branch: jest.Mock<(options: P<'branch'>) => R<'branch'>>
status: jest.Mock<(options: P<'status'>) => R<'status'>>
log: jest.Mock<(options: P<'log'>) => R<'log'>>
} = {
revparse: jest.fn(),
branch: jest.fn(),
status: jest.fn(),
log: jest.fn(),
}
jest.mock('simple-git', () => {
// use a module factory to return the stubbed SimpleGit instance
// @see https://jestjs.io/docs/es6-class-mocks#calling-jestmock-with-the-module-factory-parameter
return jest.fn().mockImplementation(() => {
return stubbedSimpleGit
})
})
type P<F extends keyof SimpleGit> = Parameters<SimpleGit[F]>
type R<F extends keyof SimpleGit> = ReturnType<SimpleGit[F]>
interface GitDataSourceConstructor {
new (config: GitDataSourceConfig): GitDataSource
}
type GDSImport = {
GitDataSource: GitDataSourceConstructor
}
describe('GitDataSource', () => {
let stubbedSimpleGit: {
// Parameters<> only gets the last overload defined, which is
// supposed to be the most permissive. However, SimpleGit defines
// overloads in the opposite order, and we need the one that takes
// a string.
revparse: sinon.SinonStub<[option: string], R<'revparse'>>
branch: sinon.SinonStub<P<'branch'>, R<'branch'>>
status: sinon.SinonStub<P<'status'>, R<'status'>>
log: sinon.SinonStub<P<'log'>, R<'log'>>
}
let stubbedWatchInstance: sinon.SinonStubbedInstance<Chokidar.FSWatcher>
let gitDataSourceImport: GDSImport
let fakeTimers: sinon.SinonFakeTimers
beforeEach(() => {
fakeTimers = sinon.useFakeTimers()
stubbedSimpleGit = {
revparse: sinon.stub<[option: string], R<'revparse'>>(),
branch: sinon.stub<P<'branch'>, R<'branch'>>(),
status: sinon.stub<P<'status'>, R<'status'>>(),
log: sinon.stub<P<'log'>, R<'log'>>(),
}
jest.useFakeTimers()
stubbedWatchInstance = sinon.createStubInstance(Chokidar.FSWatcher)
sinon.stub(Chokidar, 'watch').returns(stubbedWatchInstance)
gitDataSourceImport = proxyquire.noCallThru()('../../../src/sources/GitDataSource', {
'simple-git' () {
return stubbedSimpleGit
},
})
// @ts-expect-error - incorrect type to stub
jest.spyOn(Chokidar, 'watch').mockReturnValue(new EventEmitter())
})
afterEach(() => {
sinon.restore()
fakeTimers.restore()
stubbedSimpleGit.log.mockReset()
stubbedSimpleGit.revparse.mockReset()
stubbedSimpleGit.branch.mockReset()
stubbedSimpleGit.status.mockReset()
jest.useRealTimers()
})
describe('Unit', () => {
@@ -66,10 +54,10 @@ describe('GitDataSource', () => {
let gds: GitDataSource
let projectRoot: string
let branchName: string
let onBranchChange: sinon.SinonStub<[branch: string | null], void>
let onGitInfoChange: sinon.SinonStub<[specPath: string[]], void>
let onError: sinon.SinonStub<[err: any], void>
let onGitLogChange: sinon.SinonStub<[shas: string[]], void>
let onBranchChange: jest.Mock<(branch: string | null) => void>
let onGitInfoChange: jest.Mock<(specPath: string[]) => void>
let onError: jest.Mock<(err: any) => void>
let onGitLogChange: jest.Mock<(shas: string[]) => void>
const firstHashes = [
{ hash: 'abc' },
]
@@ -83,27 +71,21 @@ describe('GitDataSource', () => {
firstGitLogCall = pDefer()
secondGitLogCall = pDefer()
branchName = 'main'
onBranchChange = sinon.stub()
onGitInfoChange = sinon.stub()
onError = sinon.stub()
onGitLogChange = sinon.stub()
onBranchChange = jest.fn()
onGitInfoChange = jest.fn()
onError = jest.fn()
onGitLogChange = jest.fn()
projectRoot = '/root'
// @ts-ignore
stubbedSimpleGit.log.onFirstCall()
// @ts-expect-error
.callsFake(() => {
stubbedSimpleGit.log.mockImplementationOnce((opts: P<'log'>) => {
firstGitLogCall.resolve()
return { all: firstHashes }
})
.onSecondCall()
// @ts-expect-error
.callsFake(() => {
return { all: firstHashes } as unknown as R<'log'>
}).mockImplementationOnce((opts: P<'log'>) => {
secondGitLogCall.resolve()
return { all: secondHashes }
return { all: secondHashes } as unknown as R<'log'>
})
// #verifyGitRepo
@@ -111,12 +93,10 @@ describe('GitDataSource', () => {
// constructor verifies the repo in open mode via #refreshAllGitData, but does not wait for it :womp:
const revparseP = pDefer<void>()
// SimpleGit returns a chainable, but we only care about the promise
// @ts-expect-error
stubbedSimpleGit.revparse.callsFake(() => {
stubbedSimpleGit.revparse.mockImplementationOnce((opts: string) => {
revparseP.resolve()
return Promise.resolve(projectRoot)
return Promise.resolve(projectRoot) as unknown as R<'revparse'>
})
// wait for revparse to be called, so we can be assured that GitDataSource has initialized
@@ -128,18 +108,17 @@ describe('GitDataSource', () => {
const branchP = pDefer<void>()
// again, ignoring type warning re: chaining
// @ts-expect-error
stubbedSimpleGit.branch.callsFake(() => {
stubbedSimpleGit.branch.mockImplementationOnce((opts: P<'branch'>) => {
branchP.resolve()
return Promise.resolve({ current: branchName })
return Promise.resolve({ current: branchName }) as unknown as R<'branch'>
})
const onBranchChangeP = pDefer<void>()
onBranchChange.callsFake(() => onBranchChangeP.resolve())
onBranchChange.mockImplementationOnce(() => onBranchChangeP.resolve())
gds = new gitDataSourceImport.GitDataSource({
gds = new GitDataSource({
isRunMode: false,
projectRoot,
onBranchChange,
@@ -151,7 +130,7 @@ describe('GitDataSource', () => {
await revparseP.promise
await branchP.promise
await onBranchChangeP.promise
expect(onBranchChange).to.be.calledWith(branchName)
expect(onBranchChange).toHaveBeenCalledWith(branchName)
})
describe('.get currentHashes', () => {
@@ -161,15 +140,15 @@ describe('GitDataSource', () => {
})
it('returns the current hashes', () => {
expect(gds.currentHashes).to.have.same.members(firstHashesReturnValue)
expect(gds.currentHashes).toEqual(firstHashesReturnValue)
})
})
describe('after sixty seconds, when there are additional hashes', () => {
it('returns the current hashes', async () => {
await fakeTimers.tickAsync(60001)
await jest.advanceTimersByTimeAsync(60001)
await secondGitLogCall.promise
expect(gds.currentHashes).to.have.same.members(secondHashesReturnValue)
expect(gds.currentHashes).toEqual(secondHashesReturnValue)
})
})
})
@@ -1,5 +1,6 @@
import { expect } from 'chai'
import { describe, expect, it, beforeEach, afterEach } from '@jest/globals'
import dedent from 'dedent'
import path from 'path'
import { execute, ExecutionResult, parse, subscribe } from 'graphql'
import { DataContext } from '../../../src'
import { createTestDataContext, scaffoldProject } from '../helper'
@@ -40,6 +41,7 @@ describe('GraphQLDataSource', () => {
})
afterEach(() => {
process.chdir(path.join(__dirname, '../../../'))
pushFragmentIterator.return()
ctx.destroy()
})
@@ -57,18 +59,18 @@ describe('GraphQLDataSource', () => {
const result = await executeQuery(`{ cloudViewer { id } }`)
// Initial cloudViewer result returns null
expect(result.data.cloudViewer).to.eq(null)
expect(result.data.cloudViewer).toEqual(null)
const { target, data, fragment } = (await pushFragmentNextVal).data.pushFragment[0]
expect(target).to.eq('Query')
expect(data).to.eql({
expect(target).toEqual('Query')
expect(data).toEqual({
cloudViewer: {
id: 'Q2xvdWRVc2VyOjE=',
},
})
expect(fragment.trim()).to.eq(dedent`
expect(fragment.trim()).toEqual(dedent`
fragment GeneratedFragment on Query {
cloudViewer {
id
@@ -83,12 +85,12 @@ describe('GraphQLDataSource', () => {
const result = await executeQuery(`{ currentProject { id cloudProject { __typename ... on CloudProject { id name } } } }`)
// Initial cloudProject result returns null
expect(result.data.currentProject.cloudProject).to.eq(null)
expect(result.data.currentProject.cloudProject).toBeNull()
const { target, data, fragment } = (await pushFragmentNextVal).data.pushFragment[0]
expect(target).to.eq('CurrentProject')
expect(data).to.eql({
expect(target).toEqual('CurrentProject')
expect(data).toEqual({
__typename: 'CurrentProject',
cloudProject: {
__typename: 'CloudProject',
@@ -98,7 +100,7 @@ describe('GraphQLDataSource', () => {
id: Buffer.from(`CurrentProject:${projectPath}`, 'utf8').toString('base64'),
})
expect(fragment.trim()).to.eq(dedent`
expect(fragment.trim()).toEqual(dedent`
fragment GeneratedFragment on CurrentProject {
id
cloudProject {
@@ -116,7 +118,7 @@ describe('GraphQLDataSource', () => {
const result = await executeQuery(`{ cloudViewer { id } }`)
// Initial cloudProject result returns null
expect(result.data.cloudViewer).to.eql(null)
expect(result.data.cloudViewer).toBeNull()
await pushFragmentNextVal
pushFragmentNextVal = pushFragmentIterator.next().then(({ value }) => value)
@@ -124,19 +126,19 @@ describe('GraphQLDataSource', () => {
const result2 = await executeQuery(`{ cloudViewer { id cloudOrganizationsUrl } }`)
// Initial cloudProject result returns null
expect(result2.data.cloudViewer).to.eql({ id: 'Q2xvdWRVc2VyOjE=', cloudOrganizationsUrl: null })
expect(result2.data.cloudViewer).toEqual({ id: 'Q2xvdWRVc2VyOjE=', cloudOrganizationsUrl: null })
const { target, data, fragment } = (await pushFragmentNextVal).data.pushFragment[0]
expect(target).to.eq('Query')
expect(data).to.eql({
expect(target).toEqual('Query')
expect(data).toEqual({
cloudViewer: {
id: 'Q2xvdWRVc2VyOjE=',
cloudOrganizationsUrl: 'http://dummy.cypress.io/organizations',
},
})
expect(fragment.trim()).to.eq(dedent`
expect(fragment.trim()).toEqual(dedent`
fragment GeneratedFragment on Query {
cloudViewer {
id
@@ -1,13 +1,11 @@
import chai from 'chai'
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import os from 'os'
import fs from 'fs-extra'
import { matchedSpecs, transformSpec, SpecWithRelativeRoot, getLongestCommonPrefixFromPaths, getPathFromSpecPattern } from '../../../src/sources'
import path from 'path'
import sinon from 'sinon'
import chokidar from 'chokidar'
import _ from 'lodash'
import sinonChai from 'sinon-chai'
import { FoundSpec } from '@packages/types'
import { DataContext } from '../../../src'
import type { FindSpecs } from '../../../src/actions'
@@ -15,9 +13,6 @@ import { createTestDataContext } from '../helper'
import { defaultExcludeSpecPattern, defaultSpecPattern } from '@packages/config'
import FixturesHelper from '@tooling/system-tests'
chai.use(sinonChai)
const { expect } = chai
function delay (ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
@@ -25,7 +20,7 @@ function delay (ms) {
}
describe('matchedSpecs', () => {
context('got a single spec pattern from --spec via cli', () => {
describe('got a single spec pattern from --spec via cli', () => {
it('returns spec name only', () => {
const result = matchedSpecs({
projectRoot: '/var/folders/T/cy-projects/e2e',
@@ -48,11 +43,11 @@ describe('matchedSpecs', () => {
specType: 'integration',
}]
expect(result).to.eql(actual)
expect(result).toEqual(actual)
})
})
context('got a multi spec pattern from --spec via cli', () => {
describe('got a multi spec pattern from --spec via cli', () => {
it('removes all common path', () => {
const result = matchedSpecs({
projectRoot: '/var/folders/T/cy-projects/e2e',
@@ -71,14 +66,14 @@ describe('matchedSpecs', () => {
],
})
expect(result[0].relativeToCommonRoot).to.eq('simple_passing_spec.js')
expect(result[1].relativeToCommonRoot).to.eq('simple_hooks_spec.js')
expect(result[2].relativeToCommonRoot).to.eq('simple_failing_spec.js')
expect(result[3].relativeToCommonRoot).to.eq('simple_failing_hook_spec.js')
expect(result[0].relativeToCommonRoot).toEqual('simple_passing_spec.js')
expect(result[1].relativeToCommonRoot).toEqual('simple_hooks_spec.js')
expect(result[2].relativeToCommonRoot).toEqual('simple_failing_spec.js')
expect(result[3].relativeToCommonRoot).toEqual('simple_failing_hook_spec.js')
})
})
context('generic glob from config', () => {
describe('generic glob from config', () => {
it('infers common path from glob and returns spec name', () => {
const result = matchedSpecs({
projectRoot: '/Users/lachlan/code/work/cypress6/packages/app',
@@ -90,12 +85,12 @@ describe('matchedSpecs', () => {
specPattern: 'cypress/e2e/integration/**/*.spec.ts',
})
expect(result[0].relativeToCommonRoot).to.eq('files.spec.ts')
expect(result[1].relativeToCommonRoot).to.eq('index.spec.ts')
expect(result[0].relativeToCommonRoot).toEqual('files.spec.ts')
expect(result[1].relativeToCommonRoot).toEqual('index.spec.ts')
})
})
context('deeply nested test', () => {
describe('deeply nested test', () => {
it('removes superfluous leading directories', () => {
const result = matchedSpecs({
projectRoot: '/var/folders/y5/T/cy-projects/e2e',
@@ -106,7 +101,7 @@ describe('matchedSpecs', () => {
specPattern: '/var/folders/y5/T/cy-projects/e2e/cypress/integration/nested-1/nested-2/screenshot_nested_file_spec.js',
})
expect(result[0].relativeToCommonRoot).to.eq('screenshot_nested_file_spec.js')
expect(result[0].relativeToCommonRoot).toEqual('screenshot_nested_file_spec.js')
})
})
})
@@ -134,7 +129,7 @@ describe('transformSpec', () => {
relativeToCommonRoot: 'C:/Windows/Project/src/spec.cy.js',
}
expect(result).to.eql(actual)
expect(result).toEqual(actual)
})
})
@@ -176,7 +171,7 @@ describe('findSpecs', () => {
additionalIgnorePattern: [],
})
expect(specs).to.have.length(3)
expect(specs).toHaveLength(3)
})
it('find all the *.cy.{ts,js} excluding the e2e', async () => {
@@ -189,7 +184,7 @@ describe('findSpecs', () => {
additionalIgnorePattern: ['e2e/*.{spec,cy}.{ts,js}'],
})
expect(specs).to.have.length(2)
expect(specs).toHaveLength(2)
})
it('find all the *.{cy,spec}.{ts,js} excluding the e2e', async () => {
@@ -202,7 +197,7 @@ describe('findSpecs', () => {
additionalIgnorePattern: ['e2e/*.{spec,cy}.{ts,js}'],
})
expect(specs).to.have.length(3)
expect(specs).toHaveLength(3)
})
it('find all the e2e specs', async () => {
@@ -215,7 +210,7 @@ describe('findSpecs', () => {
additionalIgnorePattern: [],
})
expect(specs).to.have.length(3)
expect(specs).toHaveLength(3)
})
it('ignores node_modules if excludeSpecPattern is empty array', async () => {
@@ -228,7 +223,7 @@ describe('findSpecs', () => {
additionalIgnorePattern: [],
})
expect(specs).to.have.length(6)
expect(specs).toHaveLength(6)
})
it('ignores e2e tests if additionalIgnorePattern is set', async () => {
@@ -241,7 +236,7 @@ describe('findSpecs', () => {
excludeSpecPattern: [],
})
expect(specs).to.have.length(3)
expect(specs).toHaveLength(3)
})
it('respects excludeSpecPattern', async () => {
@@ -254,7 +249,7 @@ describe('findSpecs', () => {
excludeSpecPattern: ['**/*'],
})
expect(specs).to.have.length(0)
expect(specs).toHaveLength(0)
})
})
@@ -265,7 +260,7 @@ describe('getLongestCommonPrefixFromPaths', () => {
'cypress/component/bar/meta-component-test.cy.ts',
])
expect(lcp).to.equal('cypress/component')
expect(lcp).toEqual('cypress/component')
})
it('with src and cypress', () => {
@@ -275,7 +270,7 @@ describe('getLongestCommonPrefixFromPaths', () => {
'src/frontend/MyComponent.cy.ts',
])
expect(lcp).to.equal('')
expect(lcp).toEqual('')
})
it('with src', () => {
@@ -284,7 +279,7 @@ describe('getLongestCommonPrefixFromPaths', () => {
'src/MyComponent.cy.ts',
])
expect(lcp).to.equal('src')
expect(lcp).toEqual('src')
})
it('with 1 path', () => {
@@ -292,133 +287,133 @@ describe('getLongestCommonPrefixFromPaths', () => {
'src/frontend/MyComponent.cy.ts',
])
expect(lcp).to.equal('src/frontend')
expect(lcp).toEqual('src/frontend')
})
})
describe('getPathFromSpecPattern', () => {
context('dirname', () => {
describe('dirname', () => {
it('returns pattern without change if it is do not a glob', () => {
const specPattern = 'cypress/e2e/foo.spec.ts'
const defaultFileName = getPathFromSpecPattern({ specPattern, testingType: 'e2e' })
expect(defaultFileName).to.eq(specPattern)
expect(defaultFileName).toEqual(specPattern)
})
it('remove ** from glob if it is not in the beginning', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/**/foo.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
expect(defaultFileName).toEqual('cypress/foo.spec.ts')
})
it('replace ** for cypress if it starts with **', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: '**/e2e/foo.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
expect(defaultFileName).toEqual('cypress/e2e/foo.spec.ts')
})
it('replace ** for cypress if it starts with ** and omit extra **', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: '**/**/foo.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/foo.spec.ts')
expect(defaultFileName).toEqual('cypress/foo.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: '{cypress,tests}/{integration,e2e}/foo.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/integration/foo.spec.ts')
expect(defaultFileName).toEqual('cypress/integration/foo.spec.ts')
})
})
context('filename', () => {
describe('filename', () => {
it('replace * for filename', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/*.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/spec.spec.ts')
expect(defaultFileName).toEqual('cypress/e2e/spec.spec.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/{foo,filename}.spec.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/foo.spec.ts')
expect(defaultFileName).toEqual('cypress/e2e/foo.spec.ts')
})
})
context('test extension', () => {
describe('test extension', () => {
it('replace * for filename', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.*.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
expect(defaultFileName).toEqual('cypress/e2e/filename.cy.ts')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.{spec,cy}.ts', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/filename.spec.ts')
expect(defaultFileName).toEqual('cypress/e2e/filename.spec.ts')
})
})
context('lang extension', () => {
describe('lang extension', () => {
it('if project use TS, set TS as extension if it exists in the glob', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.cy.ts', testingType: 'e2e', fileExtensionToUse: 'ts' })
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
expect(defaultFileName).toEqual('cypress/e2e/filename.cy.ts')
})
it('if project use TS, set TS as extension if it exists in the options of extensions', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.cy.{js,ts,tsx}', testingType: 'e2e', fileExtensionToUse: 'ts' })
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
expect(defaultFileName).toEqual('cypress/e2e/filename.cy.ts')
})
it('if project use TS, do not set TS as extension if it do not exists in the options of extensions', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.cy.{js,jsx}', testingType: 'e2e', fileExtensionToUse: 'ts' })
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.js')
expect(defaultFileName).toEqual('cypress/e2e/filename.cy.js')
})
it('selects first option if there are multiples possibilities of values', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/e2e/filename.cy.{ts,js}', testingType: 'e2e' })
expect(defaultFileName).to.eq('cypress/e2e/filename.cy.ts')
expect(defaultFileName).toEqual('cypress/e2e/filename.cy.ts')
})
})
context('extra cases', () => {
describe('extra cases', () => {
it('creates specName for tests/*.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'tests/*.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('tests/spec.js')
expect(defaultFileName).toEqual('tests/spec.js')
})
it('creates specName for src/*-test.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'src/*-test.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('src/spec-test.js')
expect(defaultFileName).toEqual('src/spec-test.js')
})
it('creates specName for src/*.foo.bar.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'src/*.foo.bar.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('src/spec.foo.bar.js')
expect(defaultFileName).toEqual('src/spec.foo.bar.js')
})
it('creates specName for src/prefix.*.test.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'src/prefix.*.test.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('src/prefix.cy.test.js')
expect(defaultFileName).toEqual('src/prefix.cy.test.js')
})
it('creates specName for src/*/*.test.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'src/*/*.test.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('src/e2e/spec.test.js')
expect(defaultFileName).toEqual('src/e2e/spec.test.js')
})
it('creates specName for src-*/**/*.test.js', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'src-*/**/*.test.js', testingType: 'e2e' })
expect(defaultFileName).to.eq('src-e2e/spec.test.js')
expect(defaultFileName).toEqual('src-e2e/spec.test.js')
})
it('creates specName for src/*.test.(js|jsx)', () => {
@@ -426,7 +421,7 @@ describe('getPathFromSpecPattern', () => {
const possiblesFileNames = ['src/ComponentName.test.jsx', 'src/ComponentName.test.js']
expect(possiblesFileNames.includes(defaultFileName)).to.eq(true)
expect(possiblesFileNames.includes(defaultFileName)).toEqual(true)
})
it('creates specName for (src|components)/**/*.test.js', () => {
@@ -434,19 +429,19 @@ describe('getPathFromSpecPattern', () => {
const possiblesFileNames = ['src/ComponentName.test.js', 'components/ComponentName.test.js']
expect(possiblesFileNames.includes(defaultFileName)).to.eq(true)
expect(possiblesFileNames.includes(defaultFileName)).toEqual(true)
})
it('creates specName for e2e/**/*.cy.{js,jsx,ts,tsx}', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'e2e/**/*.cy.{js,jsx,ts,tsx}', testingType: 'e2e' })
expect(defaultFileName).to.eq('e2e/spec.cy.js')
expect(defaultFileName).toEqual('e2e/spec.cy.js')
})
it('creates specName for cypress/component-tests/**/*', () => {
const defaultFileName = getPathFromSpecPattern({ specPattern: 'cypress/component-tests/**/*', testingType: 'component', fileExtensionToUse: 'ts' })
expect(defaultFileName).to.eq('cypress/component-tests/ComponentName.cy.ts')
expect(defaultFileName).toEqual('cypress/component-tests/ComponentName.cy.ts')
})
})
})
@@ -454,18 +449,17 @@ describe('getPathFromSpecPattern', () => {
describe('_makeSpecWatcher', () => {
let ctx: DataContext
let specWatcher: chokidar.FSWatcher
let specWatcherPath: string
beforeEach(async function () {
this.timeout(20000) // fixture cleanup can take awhile
FixturesHelper.remove()
this.specWatcherPath = await FixturesHelper.scaffoldProject('spec-watcher')
specWatcherPath = await FixturesHelper.scaffoldProject('spec-watcher')
ctx = createTestDataContext('open', { projectRoot: this.specWatcherPath })
})
ctx = createTestDataContext('open', { projectRoot: specWatcherPath })
}, 20000)
afterEach(async () => {
sinon.restore()
await specWatcher.close()
ctx.destroy()
})
@@ -488,7 +482,7 @@ describe('_makeSpecWatcher', () => {
it('watch for changes on files based on the specPattern', async function () {
specWatcher = ctx.project._makeSpecWatcher({
projectRoot: this.specWatcherPath,
projectRoot: specWatcherPath,
specPattern: ['**/*.{cy,spec}.{ts,js}'],
excludeSpecPattern: ['**/ignore.spec.ts'],
additionalIgnorePattern: ['additional.ignore.cy.js'],
@@ -509,18 +503,18 @@ describe('_makeSpecWatcher', () => {
await delay(10)
}
expect(Array.from(allFiles).sort()).to.eql([
expect(Array.from(allFiles).sort()).toEqual([
SPEC_FILE1,
SPEC_FILE2,
SPEC_FILE3,
])
expect(Array.from(allFiles)).to.not.include(SUPPORT_FILE)
expect(Array.from(allFiles)).not.toContain(SUPPORT_FILE)
})
it('watch for changes on files with multiple specPatterns', async function () {
specWatcher = ctx.project._makeSpecWatcher({
projectRoot: this.specWatcherPath,
projectRoot: specWatcherPath,
specPattern: ['**/*.{cy,spec}.{ts,js}', '**/abc.ts'],
excludeSpecPattern: ['**/ignore.spec.ts'],
additionalIgnorePattern: ['additional.ignore.cy.js'],
@@ -541,19 +535,19 @@ describe('_makeSpecWatcher', () => {
await delay(10)
}
expect(Array.from(allFiles).sort()).to.eql([
expect(Array.from(allFiles).sort()).toEqual([
SPEC_FILE1,
SPEC_FILE_ABC,
SPEC_FILE2,
SPEC_FILE3,
])
expect(Array.from(allFiles)).to.not.include(SUPPORT_FILE)
expect(Array.from(allFiles)).not.toContain(SUPPORT_FILE)
})
it('do not throw if file/folder is deleted while ignoring files', async function () {
specWatcher = ctx.project._makeSpecWatcher({
projectRoot: this.specWatcherPath,
projectRoot: specWatcherPath,
specPattern: ['**/*.{cy,spec}.{ts,js}', '**/abc.ts'],
excludeSpecPattern: ['**/ignore.spec.ts'],
additionalIgnorePattern: ['additional.ignore.cy.js'],
@@ -571,14 +565,14 @@ describe('_makeSpecWatcher', () => {
await ctx.actions.file.removeFileInProject(SPEC_FILE1)
await delay(1000)
expect(Array.from(allFiles).sort()).to.eql([
expect(Array.from(allFiles).sort()).toEqual([
SPEC_FILE_ABC,
SPEC_FILE2,
SPEC_FILE3,
])
expect(Array.from(allFiles)).to.not.include(SPEC_FILE1)
expect(Array.from(allFiles)).to.not.include(SUPPORT_FILE)
expect(Array.from(allFiles)).not.toContain(SPEC_FILE1)
expect(Array.from(allFiles)).not.toContain(SUPPORT_FILE)
})
})
@@ -587,10 +581,6 @@ describe('startSpecWatcher', () => {
let ctx: DataContext
afterEach(async () => {
sinon.restore()
})
describe('run mode', () => {
beforeEach(async () => {
ctx = createTestDataContext('run')
@@ -599,9 +589,9 @@ describe('startSpecWatcher', () => {
})
it('early return specWatcher', () => {
const onStub = sinon.stub()
const onStub = jest.fn()
sinon.stub(chokidar, 'watch').callsFake(() => {
jest.spyOn(chokidar, 'watch').mockImplementation(() => {
const mockWatcher = {
on: onStub,
close: () => ({ catch: () => {} }),
@@ -612,7 +602,7 @@ describe('startSpecWatcher', () => {
let handleFsChange
sinon.stub(_, 'debounce').callsFake((funcToDebounce) => {
jest.spyOn(_, 'debounce').mockImplementation((funcToDebounce) => {
handleFsChange = (() => funcToDebounce())
return handleFsChange as _.DebouncedFunc<any>
@@ -627,11 +617,11 @@ describe('startSpecWatcher', () => {
additionalIgnorePattern: ['additional.ignore.cy.js'],
})
expect(_.debounce).to.have.not.been.called
expect(_.debounce).not.toHaveBeenCalled()
expect(chokidar.watch).to.have.not.been.called
expect(chokidar.watch).not.toHaveBeenCalled()
expect(onStub).to.have.not.been.called
expect(onStub).not.toHaveBeenCalled()
})
})
@@ -662,9 +652,9 @@ describe('startSpecWatcher', () => {
})
it('creates file watcher based on given config properties', async () => {
const onStub = sinon.stub()
const onStub = jest.fn()
sinon.stub(chokidar, 'watch').callsFake(() => {
jest.spyOn(chokidar, 'watch').mockImplementation(() => {
const mockWatcher = {
on: onStub,
close: () => ({ catch: () => {} }),
@@ -675,7 +665,7 @@ describe('startSpecWatcher', () => {
let handleFsChange
sinon.stub(_, 'debounce').callsFake((funcToDebounce) => {
jest.spyOn(_, 'debounce').mockImplementation((funcToDebounce) => {
handleFsChange = (() => funcToDebounce())
return handleFsChange as _.DebouncedFunc<any>
@@ -690,16 +680,16 @@ describe('startSpecWatcher', () => {
additionalIgnorePattern: ['additional.ignore.cy.js'],
})
expect(_.debounce).to.have.been.calledWith(sinon.match.func, 250)
expect(_.debounce).toHaveBeenCalledWith(expect.any(Function), 250)
expect(chokidar.watch).to.have.been.calledWith('.', {
expect(chokidar.watch).toHaveBeenCalledWith('.', {
ignoreInitial: true,
cwd: projectRoot,
ignored: ['**/node_modules/**', '**/ignore.spec.ts', 'additional.ignore.cy.js', sinon.match.func],
ignored: ['**/node_modules/**', '**/ignore.spec.ts', 'additional.ignore.cy.js', expect.any(Function)],
ignorePermissionErrors: true,
})
expect(onStub).to.have.been.calledWith('all', handleFsChange)
expect(onStub).toHaveBeenCalledWith('all', handleFsChange)
})
it('implements change handler with duplicate result handling', async () => {
@@ -709,10 +699,11 @@ describe('startSpecWatcher', () => {
{ name: 'test-3.cy.js' },
] as FoundSpec[]
sinon.stub(ctx.project, 'findSpecs').resolves(mockFoundSpecs)
sinon.stub(ctx.actions.project, 'setSpecs')
// @ts-expect-error
jest.spyOn(ctx.project, 'findSpecs').mockResolvedValue(mockFoundSpecs)
jest.spyOn(ctx.actions.project, 'setSpecs')
sinon.stub(chokidar, 'watch').callsFake(() => {
jest.spyOn(chokidar, 'watch').mockImplementation(() => {
const mockWatcher = {
on: () => {},
close: () => ({ catch: () => {} }),
@@ -723,7 +714,7 @@ describe('startSpecWatcher', () => {
let handleFsChange
sinon.stub(_, 'debounce').callsFake((funcToDebounce) => {
jest.spyOn(_, 'debounce').mockImplementation((funcToDebounce) => {
handleFsChange = (() => funcToDebounce())
return handleFsChange as _.DebouncedFunc<any>
@@ -741,22 +732,25 @@ describe('startSpecWatcher', () => {
await ctx.project.startSpecWatcher(watchOptions)
// Set internal specs state to the stubbed found value to simulate irrelevant FS changes
// @ts-expect-error
ctx.project.setSpecs(mockFoundSpecs)
await handleFsChange()
expect(ctx.project.findSpecs).to.have.been.calledWith(watchOptions)
expect(ctx.actions.project.setSpecs).not.to.have.been.called
expect(ctx.project.findSpecs).toHaveBeenCalledWith(watchOptions)
expect(ctx.actions.project.setSpecs).not.toHaveBeenCalled()
// Update internal specs state so that a change will be detected on next FS event
const updatedSpecs = [...mockFoundSpecs, { name: 'test-4.cy.js' }] as FoundSpec[]
// @ts-expect-error
ctx.project.setSpecs(updatedSpecs)
await handleFsChange()
expect(ctx.project.findSpecs).to.have.been.calledWith(watchOptions)
expect(ctx.actions.project.setSpecs).to.have.been.calledWith(mockFoundSpecs)
expect(ctx.project.findSpecs).toHaveBeenCalledWith(watchOptions)
// @ts-expect-error
expect(ctx.actions.project.setSpecs).toHaveBeenCalledWith(mockFoundSpecs)
})
})
})
@@ -770,24 +764,25 @@ describe('ProjectDataSource', () => {
ctx.coreData.currentTestingType = 'e2e'
})
context('#defaultSpecFilename', () => {
describe('#defaultSpecFilename', () => {
it('yields default if no spec pattern is set', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: [] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: [] })
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/e2e/spec.cy.js')
expect(defaultSpecFileName).toEqual('cypress/e2e/spec.cy.js')
})
it('yields default if the spec pattern is default', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: [defaultSpecPattern.e2e] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: [defaultSpecPattern.e2e] })
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/e2e/spec.cy.js')
expect(defaultSpecFileName).toEqual('cypress/e2e/spec.cy.js')
})
it('yields common prefix if there are existing specs', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: ['cypress/e2e/**/*'] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: ['cypress/e2e/**/*'] })
// @ts-expect-error
ctx.project.setSpecs([
{ relative: 'cypress/e2e/foo/spec.js' },
{ relative: 'cypress/e2e/foo/bar/spec.js' },
@@ -795,20 +790,23 @@ describe('ProjectDataSource', () => {
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/e2e/foo/spec.cy.js')
expect(defaultSpecFileName).toEqual('cypress/e2e/foo/spec.cy.js')
})
it('yields spec pattern guess if there are no existing specs', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: ['cypress/integration/**/*'] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: ['cypress/integration/**/*'] })
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/integration/spec.cy.js')
expect(defaultSpecFileName).toEqual('cypress/integration/spec.cy.js')
})
it('yields correct filename from specpattern if there are existing specs', async () => {
jest.spyOn(ctx.lifecycleManager, 'getConfigFileContents').mockResolvedValue({})
ctx.coreData.currentTestingType = 'component'
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: ['cypress/component-tests/*.spec.js'] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: ['cypress/component-tests/*.spec.js'] })
// @ts-expect-error
ctx.project.setSpecs([
{ relative: 'cypress/component-tests/foo/spec.spec.js' },
{ relative: 'cypress/component-tests/foo/spec2.spec.js' },
@@ -816,7 +814,7 @@ describe('ProjectDataSource', () => {
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/component-tests/foo/ComponentName.spec.js')
expect(defaultSpecFileName).toEqual('cypress/component-tests/foo/ComponentName.spec.js')
})
describe('jsx/tsx handling', () => {
@@ -826,30 +824,30 @@ describe('ProjectDataSource', () => {
})
it('yields correct jsx extension if there are jsx files and specPattern allows', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: [defaultSpecPattern.component] })
sinon.stub(ctx.project, 'specPatternsByTestingType').resolves({ specPattern: [defaultSpecPattern.component] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: [defaultSpecPattern.component] })
jest.spyOn(ctx.project, 'specPatternsByTestingType').mockResolvedValue({ specPattern: [defaultSpecPattern.component] })
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
expect(defaultSpecFileName).to.equal('cypress/component/ComponentName.cy.jsx', defaultSpecFileName)
expect(defaultSpecFileName).toEqual('cypress/component/ComponentName.cy.jsx', defaultSpecFileName)
})
it('yields non-jsx extension if there are jsx files but specPattern disallows', async () => {
sinon.stub(ctx.project, 'specPatterns').resolves({ specPattern: ['cypress/component/*.cy.js'] })
sinon.stub(ctx.project, 'specPatternsByTestingType').resolves({ specPattern: ['cypress/component/*.cy.js'] })
jest.spyOn(ctx.project, 'specPatterns').mockResolvedValue({ specPattern: ['cypress/component/*.cy.js'] })
jest.spyOn(ctx.project, 'specPatternsByTestingType').mockResolvedValue({ specPattern: ['cypress/component/*.cy.js'] })
const defaultSpecFileName = await ctx.project.defaultSpecFileName()
// specPattern does not allow for jsx, so generated spec name should not use jsx extension
expect(defaultSpecFileName).to.equal('cypress/component/ComponentName.cy.js', defaultSpecFileName)
expect(defaultSpecFileName).toEqual('cypress/component/ComponentName.cy.js', defaultSpecFileName)
})
})
})
describe('specPatternsByTestingType', () => {
context('when custom patterns configured', () => {
describe('when custom patterns configured', () => {
beforeEach(() => {
sinon.stub(ctx.lifecycleManager, 'getConfigFileContents').resolves({
jest.spyOn(ctx.lifecycleManager, 'getConfigFileContents').mockResolvedValue({
e2e: {
specPattern: 'abc',
excludeSpecPattern: 'def',
@@ -862,38 +860,38 @@ describe('ProjectDataSource', () => {
})
it('should return custom e2e patterns', async () => {
expect(await ctx.project.specPatternsByTestingType('e2e')).to.eql({
expect(await ctx.project.specPatternsByTestingType('e2e')).toEqual({
specPattern: ['abc'],
excludeSpecPattern: ['def'],
})
})
it('should return custom component patterns', async () => {
expect(await ctx.project.specPatternsByTestingType('component')).to.eql({
expect(await ctx.project.specPatternsByTestingType('component')).toEqual({
specPattern: ['uvw'],
excludeSpecPattern: ['xyz'],
})
})
})
context('when no custom patterns configured', () => {
describe('when no custom patterns configured', () => {
const wrapInArray = (value: string | string[]): string[] => {
return Array.isArray(value) ? value : [value]
}
beforeEach(() => {
sinon.stub(ctx.lifecycleManager, 'getConfigFileContents').resolves({})
jest.spyOn(ctx.lifecycleManager, 'getConfigFileContents').mockResolvedValue({})
})
it('should return default e2e patterns', async () => {
expect(await ctx.project.specPatternsByTestingType('e2e')).to.eql({
expect(await ctx.project.specPatternsByTestingType('e2e')).toEqual({
specPattern: wrapInArray(defaultSpecPattern.e2e),
excludeSpecPattern: wrapInArray(defaultExcludeSpecPattern.e2e),
})
})
it('should return default component patterns', async () => {
expect(await ctx.project.specPatternsByTestingType('component')).to.eql({
expect(await ctx.project.specPatternsByTestingType('component')).toEqual({
specPattern: wrapInArray(defaultSpecPattern.component),
excludeSpecPattern: wrapInArray(defaultExcludeSpecPattern.component),
})
@@ -1,6 +1,4 @@
import chai from 'chai'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import debugLib from 'debug'
import { GraphQLInt, GraphQLString, print } from 'graphql'
@@ -11,9 +9,6 @@ import { FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC } from './fixtures/graphqlFixture
import { createGraphQL } from '../helper-graphql'
import dedent from 'dedent'
chai.use(sinonChai)
const { expect } = chai
const debug = debugLib('cypress:data-context:test:sources:RelevantRunSpecsDataSource')
describe('RelevantRunSpecsDataSource', () => {
@@ -23,38 +18,37 @@ describe('RelevantRunSpecsDataSource', () => {
beforeEach(() => {
ctx = createTestDataContext('open')
dataSource = new RelevantRunSpecsDataSource(ctx)
sinon.stub(ctx.project, 'projectId').resolves('test123')
jest.spyOn(ctx.project, 'projectId').mockResolvedValue('test123')
})
describe('getRelevantRunSpecs()', () => {
it('returns no specs or statuses when no specs found for run', async () => {
const result = await dataSource.getRelevantRunSpecs([])
expect(result).to.eql([])
expect(result).toEqual([])
})
it('returns the runs the cloud sends and sets the polling interval', async () => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC)
// @ts-expect-error
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockResolvedValue(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC)
expect(dataSource.pollingInterval).to.eql(15)
expect(dataSource.pollingInterval).toEqual(15)
const result = await dataSource.getRelevantRunSpecs(['fake-id'])
expect(result).to.eql(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC.data.cloudNodesByIds)
expect(result).toEqual(FAKE_PROJECT_ONE_RUNNING_RUN_ONE_SPEC.data.cloudNodesByIds)
expect(dataSource.pollingInterval).to.eql(20)
expect(dataSource.pollingInterval).toEqual(20)
})
})
describe('polling', () => {
let clock: sinon.SinonFakeTimers
beforeEach(() => {
clock = sinon.useFakeTimers()
jest.useFakeTimers()
})
afterEach(() => {
clock.restore()
jest.useRealTimers()
})
it('polls and emits changes', async () => {
@@ -65,7 +59,8 @@ describe('RelevantRunSpecsDataSource', () => {
const runId = testData.data.cloudNodesByIds[0].id
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT)
// @ts-expect-error
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockResolvedValue(FAKE_PROJECT)
const query = `
query Test {
@@ -101,72 +96,78 @@ describe('RelevantRunSpecsDataSource', () => {
},
}
return createGraphQL(query, fields, async (source, args, context, info) => {
const result = await createGraphQL(query, fields, async (source, args, context, info) => {
const subscriptionIterator = dataSource.pollForSpecs(runId, info)
const firstEmit = await subscriptionIterator.next()
expect(firstEmit, 'should emit because of first value').to.eql({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
// should emit because of first value
expect(firstEmit).toEqual({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
FAKE_PROJECT.data.cloudNodesByIds[0].totalInstanceCount++
debug('**** tick after total instance count increase')
await clock.nextAsync()
await jest.runOnlyPendingTimers()
const secondEmit = await subscriptionIterator.next()
expect(secondEmit, 'should emit because of updated "totalInstanceCount"').to.eql({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
// should emit because of updated "totalInstanceCount"
expect(secondEmit).toEqual({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
FAKE_PROJECT.data.cloudNodesByIds[0].scheduledToCompleteAt = (new Date()).toISOString()
debug('**** tick after adding scheduledToCompleteAt')
await clock.nextAsync()
await jest.runOnlyPendingTimers()
const thirdEmit = await subscriptionIterator.next()
expect(thirdEmit, 'should emit again because of updated "scheduledToCompleteAt"').to.eql({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
// should emit again because of updated "scheduledToCompleteAt"
expect(thirdEmit).toEqual({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
FAKE_PROJECT.data.cloudNodesByIds[0].totalTests++
debug('**** tick after testCounts increase')
await clock.nextAsync()
await jest.runOnlyPendingTimers()
const forthEmit = await subscriptionIterator.next()
expect(forthEmit, 'should emit again because of updated "testCounts"').to.eql({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
// should emit again because of updated "testCounts"
expect(forthEmit).toEqual({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
FAKE_PROJECT.data.cloudNodesByIds[0].status = 'FAILED'
debug('**** tick after setting status Failed')
await clock.nextAsync()
await jest.runOnlyPendingTimers()
const finalEmit = await subscriptionIterator.next()
expect(finalEmit, 'should emit again because of updated "status"').to.eql({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
// should emit again because of updated "status"
expect(finalEmit).toEqual({ done: false, value: FAKE_PROJECT.data.cloudNodesByIds[0] })
subscriptionIterator.return(undefined)
return {}
}).then((result) => {
if (result.errors) {
throw result.errors[0]
}
const expected = {
data: {
test: {
completedInstanceCount: null,
id: null,
runNumber: null,
status: null,
totalInstanceCount: null,
totalTests: null,
},
},
}
expect(result).to.eql(expected)
})
if (result.errors) {
throw result.errors[0]
}
const expected = {
data: {
test: {
completedInstanceCount: null,
id: null,
runNumber: null,
status: null,
totalInstanceCount: null,
totalTests: null,
},
},
}
expect(result).toEqual(expected)
})
it('should create query', async () => {
const gqlStub = sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves({ data: {} })
// @ts-expect-error
const gqlStub = jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockResolvedValue({ data: {} })
const fields = {
value: {
@@ -201,11 +202,11 @@ describe('RelevantRunSpecsDataSource', () => {
let iterator1: ReturnType<RelevantRunSpecsDataSource['pollForSpecs']>
let iterator2: ReturnType<RelevantRunSpecsDataSource['pollForSpecs']>
return createGraphQL(query, fields, async (source, args, context, info) => {
await createGraphQL(query, fields, async (source, args, context, info) => {
iterator1 = dataSource.pollForSpecs('runId', info)
})
.then(() => {
const expected =
const firstExpected =
dedent`query RelevantRunSpecsDataSource_Specs($ids: [ID!]!) {
cloudNodesByIds(ids: $ids) {
id
@@ -227,19 +228,20 @@ describe('RelevantRunSpecsDataSource', () => {
value2
}`
expect(gqlStub).to.have.been.called
expect(gqlStub.firstCall.args[0]).to.haveOwnProperty('operationDoc')
expect(print(gqlStub.firstCall.args[0].operationDoc), 'should match initial query with one fragment').to.eql(`${expected }\n`)
})
.then(() => {
return createGraphQL(query2, fields, async (source, args, context, info) => {
iterator2 = dataSource.pollForSpecs('runId', info)
expect(gqlStub).toHaveBeenCalled()
const gqlStubFirstCallFirstArg = gqlStub.mock.calls[0][0]
await clock.nextAsync()
})
expect(gqlStubFirstCallFirstArg).toHaveProperty('operationDoc')
// should match initial query with one fragment
expect(print(gqlStubFirstCallFirstArg.operationDoc)).toEqual(`${firstExpected }\n`)
await createGraphQL(query2, fields, async (source, args, context, info) => {
iterator2 = dataSource.pollForSpecs('runId', info)
await jest.runOnlyPendingTimers()
})
.then(() => {
const expected =
const secondExpected =
dedent`query RelevantRunSpecsDataSource_Specs($ids: [ID!]!) {
cloudNodesByIds(ids: $ids) {
id
@@ -267,14 +269,15 @@ describe('RelevantRunSpecsDataSource', () => {
value3
}`
expect(gqlStub).to.have.been.calledTwice
expect(gqlStub.secondCall.args[0]).to.haveOwnProperty('operationDoc')
expect(print(gqlStub.secondCall.args[0].operationDoc), 'should match second query with two fragments').to.eql(`${expected }\n`)
})
.then(() => {
iterator1.return(undefined)
iterator2.return(undefined)
})
expect(gqlStub).toHaveBeenCalledTimes(2)
const gqlStubSecondCallFirstArg = gqlStub.mock.calls[1][0]
expect(gqlStubSecondCallFirstArg).toHaveProperty('operationDoc')
// should match second query with two fragments
expect(print(gqlStubSecondCallFirstArg.operationDoc)).toEqual(`${secondExpected }\n`)
iterator1.return(undefined)
iterator2.return(undefined)
})
})
})
@@ -1,5 +1,4 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { describe, expect, it, beforeEach, jest } from '@jest/globals'
import debugLib from 'debug'
import { DataContext } from '../../../src'
@@ -29,32 +28,33 @@ describe('RelevantRunsDataSource', () => {
beforeEach(() => {
ctx = createTestDataContext('open')
dataSource = new RelevantRunsDataSource(ctx)
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockReset()
})
it('returns empty with no shas', async () => {
const result = await dataSource.getRelevantRuns([])
expect(result).to.eql([])
expect(result).toEqual([])
})
it('returns empty with no project set', async () => {
sinon.stub(ctx.project, 'projectId').resolves(undefined)
jest.spyOn(ctx.project, 'projectId').mockResolvedValue(undefined)
const result = await dataSource.getRelevantRuns([FAKE_SHAS[0]])
expect(result).to.eql([])
expect(result).toEqual([])
})
it('returns empty if error', async () => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(FAKE_PROJECT_WITH_ERROR)
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockResolvedValue(FAKE_PROJECT_WITH_ERROR)
const result = await dataSource.getRelevantRuns([])
expect(result).to.eql([])
expect(result).toEqual([])
})
context('cloud responses', () => {
describe('cloud responses', () => {
beforeEach(() => {
sinon.stub(ctx.project, 'projectId').resolves('test123')
jest.spyOn(ctx.project, 'projectId').mockResolvedValue('test123')
})
const getShasForTestData = (testData: TestProject) => {
@@ -62,13 +62,14 @@ describe('RelevantRunsDataSource', () => {
}
const testScenario = async (testData: TestProject, expectedResult: RelevantRunInfo[]) => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL').resolves(testData)
// @ts-expect-error
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL').mockResolvedValue(testData)
const testShas: string[] = getShasForTestData(testData)
const result = await dataSource.getRelevantRuns(testShas)
expect(result).to.eql(expectedResult)
expect(result).toEqual(expectedResult)
}
it('returns empty if cloud project not loaded', async () => {
@@ -104,30 +105,37 @@ describe('RelevantRunsDataSource', () => {
})
it('returns the same current if current already set only one running', async () => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL')
.onFirstCall().resolves(FAKE_PROJECT_ONE_RUNNING_RUN)
.onSecondCall().resolves(FAKE_PROJECT_ONE_RUNNING_RUN)
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL')
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_ONE_RUNNING_RUN)
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_ONE_RUNNING_RUN)
const firstResult = await dataSource.getRelevantRuns([FAKE_SHAS[0]])
expect(firstResult, 'running should be current after first check').to.eql(
// running should be current after first check
expect(firstResult).toEqual(
[formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
)
const secondResult = await dataSource.getRelevantRuns([FAKE_SHAS[0]])
expect(secondResult, 'running should be current after second check').to.eql(
// running should be current after second check
expect(secondResult).toEqual(
[formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
)
})
it('returns the same current if current already set and updates after movesToNext is called', async () => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL')
.onFirstCall().resolves(FAKE_PROJECT_ONE_RUNNING_RUN)
.onSecondCall().resolves(FAKE_PROJECT_MULTIPLE_COMPLETED)
.onThirdCall().resolves(FAKE_PROJECT_MULTIPLE_COMPLETED)
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL')
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_ONE_RUNNING_RUN)
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_MULTIPLE_COMPLETED)
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_MULTIPLE_COMPLETED)
const maybeSendRunNotificationStub = sinon.stub(ctx.actions.notification, 'maybeSendRunNotification')
const maybeSendRunNotificationStub = jest.spyOn(ctx.actions.notification, 'maybeSendRunNotification')
const subscription = ctx.emitter.subscribeTo('relevantRunChange')
const subValues: any[] = []
@@ -145,12 +153,13 @@ describe('RelevantRunsDataSource', () => {
debug('first check with only one running run')
await dataSource.checkRelevantRuns([FAKE_SHAS[0]], true)
expect(maybeSendRunNotificationStub).not.to.have.been.called
expect(maybeSendRunNotificationStub).not.toHaveBeenCalled()
debug('second check with the running run completing, but should stay selected')
await dataSource.checkRelevantRuns([FAKE_SHAS[1], FAKE_SHAS[0]], true)
expect(maybeSendRunNotificationStub).to.have.been.calledWithMatch(
expect(maybeSendRunNotificationStub).toHaveBeenCalledWith(
// @ts-expect-error
{ runNumber: 1, status: 'RUNNING', sha: 'fcb90f', totalFailed: 0 },
{ runNumber: 4, status: 'FAILED', sha: 'fc753a', totalFailed: 1 },
)
@@ -158,7 +167,7 @@ describe('RelevantRunsDataSource', () => {
debug('moving runs will cause another check')
await dataSource.moveToRun(4, [FAKE_SHAS[1], FAKE_SHAS[0]])
expect(maybeSendRunNotificationStub).to.have.been.calledOnce
expect(maybeSendRunNotificationStub).toHaveBeenCalledTimes(1)
setImmediate(() => {
subscription.return(undefined)
@@ -166,16 +175,18 @@ describe('RelevantRunsDataSource', () => {
await watchSubscription()
expect(subValues).to.have.lengthOf(3)
expect(subValues).toHaveLength(3)
expect(subValues[0], 'should emit first result of running').to.eql({
expect(subValues[0]).toEqual({
// should emit first result of running
all: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
commitsAhead: 0,
latest: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
selectedRunNumber: 1,
})
expect(subValues[1], 'should keep run if selected but no longer in all').to.eql({
expect(subValues[1]).toEqual({
// should keep run if selected but no longer in all
all: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 1),
@@ -188,7 +199,8 @@ describe('RelevantRunsDataSource', () => {
selectedRunNumber: 1,
})
expect(subValues[2], 'should emit selected run after moving').to.eql({
expect(subValues[2]).toEqual({
// should emit selected run after moving
all: [formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0)],
latest: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
@@ -200,9 +212,11 @@ describe('RelevantRunsDataSource', () => {
})
it('moves to new sha once completed', async () => {
sinon.stub(ctx.cloud, 'executeRemoteGraphQL')
.onFirstCall().resolves(FAKE_PROJECT_ONE_RUNNING_RUN)
.onSecondCall().resolves(FAKE_PROJECT_MULTIPLE_COMPLETED)
jest.spyOn(ctx.cloud, 'executeRemoteGraphQL')
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_ONE_RUNNING_RUN)
// @ts-expect-error
.mockResolvedValueOnce(FAKE_PROJECT_MULTIPLE_COMPLETED)
const subscription = ctx.emitter.subscribeTo('relevantRunChange')
const subValues: any[] = []
@@ -229,16 +243,18 @@ describe('RelevantRunsDataSource', () => {
await watchSubscription()
expect(subValues).to.have.lengthOf(2)
expect(subValues).toHaveLength(2)
expect(subValues[0], 'should emit first result of running').to.eql({
expect(subValues[0]).toEqual({
// should emit first result of running
all: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
latest: [formatRun(FAKE_PROJECT_ONE_RUNNING_RUN, 0)],
commitsAhead: 0,
selectedRunNumber: 1,
})
expect(subValues[1], 'should emit newer completed run on different sha').to.eql({
expect(subValues[1]).toEqual({
// should emit newer completed run on different sha
all: [formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0)],
latest: [
formatRun(FAKE_PROJECT_MULTIPLE_COMPLETED, 0),
@@ -1,4 +1,4 @@
import { expect } from 'chai'
import { describe, expect, it, beforeEach, afterEach } from '@jest/globals'
import crypto from 'crypto'
import { DataContext } from '../../../src'
@@ -26,8 +26,8 @@ describe('RemoteRequestDataSource', () => {
{ b: '2', a: 1 },
)
expect(id).to.eql('UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo1Y2MyNWQ4YTM5YTY1NGViMjNiNTI1NzM0NWFiYmY0MmJlNDBjOGQxOmV5SmhJam94TENKaUlqb2lNaUo5')
expect(Buffer.from(id, 'base64').toString('utf-8')).to.eql('RemoteFetchableCloudProjectSpecResult:5cc25d8a39a654eb23b5257345abbf42be40c8d1:eyJhIjoxLCJiIjoiMiJ9')
expect(id).toEqual('UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo1Y2MyNWQ4YTM5YTY1NGViMjNiNTI1NzM0NWFiYmY0MmJlNDBjOGQxOmV5SmhJam94TENKaUlqb2lNaUo5')
expect(Buffer.from(id, 'base64').toString('utf-8')).toEqual('RemoteFetchableCloudProjectSpecResult:5cc25d8a39a654eb23b5257345abbf42be40c8d1:eyJhIjoxLCJiIjoiMiJ9')
})
it('stable stringifies via stringifyVariables', () => {
@@ -42,13 +42,13 @@ describe('RemoteRequestDataSource', () => {
{ a: 1, b: '2' },
)
expect(id).to.eql(id2)
expect(id).toEqual(id2)
})
})
describe('unpackFetchableNodeId', () => {
it('takes the identifier created from makeRefetchableId and decodes the variables', () => {
expect(remoteRequestSource.unpackFetchableNodeId('UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo1Y2MyNWQ4YTM5YTY1NGViMjNiNTI1NzM0NWFiYmY0MmJlNDBjOGQxOmV5SmhJam94TENKaUlqb2lNaUo5')).to.eql({
expect(remoteRequestSource.unpackFetchableNodeId('UmVtb3RlRmV0Y2hhYmxlQ2xvdWRQcm9qZWN0U3BlY1Jlc3VsdDo1Y2MyNWQ4YTM5YTY1NGViMjNiNTI1NzM0NWFiYmY0MmJlNDBjOGQxOmV5SmhJam94TENKaUlqb2lNaUo5')).toEqual({
name: 'RemoteFetchableCloudProjectSpecResult',
operationHash: '5cc25d8a39a654eb23b5257345abbf42be40c8d1',
operationVariables: { a: 1, b: '2' },
@@ -1,30 +1,26 @@
import chai, { expect } from 'chai'
import { describe, expect, it, beforeEach, afterEach, jest } from '@jest/globals'
import os from 'os'
import sinon from 'sinon'
import sinonChai from 'sinon-chai'
import { Response } from 'cross-fetch'
import { DataContext } from '../../../src'
import { VersionsDataSource } from '../../../src/sources'
import { createTestDataContext } from '../helper'
import { CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
const pkg = require('@packages/root')
chai.use(sinonChai)
import { AllowedState, CYPRESS_REMOTE_MANIFEST_URL, NPM_CYPRESS_REGISTRY_URL } from '@packages/types'
import pkg from '@packages/root'
describe('VersionsDataSource', () => {
context('.versions', () => {
describe('.versions', () => {
let ctx: DataContext
let fetchStub: sinon.SinonStub
let isDependencyInstalledByNameStub: sinon.SinonStub
let fetchMock: jest.Mock
let isDependencyInstalledByNameStub: jest.Mock
let mockNow: Date = new Date()
let currentCypressVersion: string = pkg.version
beforeEach(() => {
ctx = createTestDataContext('open')
;(ctx.lifecycleManager as any)._cachedInitialConfig = {
// @ts-expect-error
ctx.lifecycleManager._cachedInitialConfig = {
component: {
devServer: {
framework: 'react',
@@ -37,59 +33,66 @@ describe('VersionsDataSource', () => {
ctx.coreData.currentProject = '/abc'
ctx.coreData.currentTestingType = 'e2e'
fetchStub = sinon.stub()
fetchMock = jest.fn()
fetchStub
.withArgs(NPM_CYPRESS_REGISTRY_URL)
.resolves({
json: sinon.stub().resolves({
'time': {
modified: '2022-01-31T21:14:41.593Z',
created: '2014-03-09T01:07:35.219Z',
[currentCypressVersion]: '2014-03-09T01:07:37.369Z',
'18.0.0': '2015-05-07T00:09:41.109Z',
},
}),
})
isDependencyInstalledByNameStub = jest.fn()
isDependencyInstalledByNameStub = sinon.stub()
sinon.stub(ctx.util, 'fetch').callsFake(fetchStub)
sinon.stub(ctx.util, 'isDependencyInstalledByName').callsFake(isDependencyInstalledByNameStub)
sinon.stub(os, 'platform').returns('darwin')
sinon.stub(os, 'arch').returns('x64')
sinon.useFakeTimers({ now: mockNow })
// @ts-expect-error
jest.spyOn(ctx.util, 'fetch').mockImplementation(fetchMock)
// @ts-expect-error
jest.spyOn(ctx.util, 'isDependencyInstalledByName').mockImplementation(isDependencyInstalledByNameStub)
jest.spyOn(os, 'platform').mockReturnValue('darwin')
jest.spyOn(os, 'arch').mockReturnValue('x64')
jest.useFakeTimers({ now: mockNow })
})
afterEach(() => {
sinon.restore()
jest.useRealTimers()
})
it('loads the manifest for the latest version with all headers and queries npm for release dates', async () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
'x-arch': 'x64',
'x-initial-launch': String(true),
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
}),
}).resolves({
json: sinon.stub().resolves({
name: 'Cypress',
version: '18.0.0',
}),
fetchMock.mockImplementation((url: string, options: { headers: Record<string, string> }) => {
if (url === NPM_CYPRESS_REGISTRY_URL) {
return Promise.resolve({
// @ts-expect-error
json: jest.fn().mockResolvedValue({
'time': {
modified: '2022-01-31T21:14:41.593Z',
created: '2014-03-09T01:07:35.219Z',
[currentCypressVersion]: '2014-03-09T01:07:37.369Z',
'18.0.0': '2015-05-07T00:09:41.109Z',
},
}),
})
}
if (
url === CYPRESS_REMOTE_MANIFEST_URL &&
options.headers['Content-Type'] === 'application/json' &&
options.headers['x-cypress-version'] === currentCypressVersion &&
options.headers['x-os-name'] === 'darwin' &&
options.headers['x-arch'] === 'x64' &&
options.headers['x-initial-launch'] === String(true) &&
options.headers['x-machine-id'] === 'abcd123' &&
options.headers['x-testing-type'] === 'e2e' &&
options.headers['x-logged-in'] === 'false') {
return Promise.resolve({
// @ts-expect-error
json: jest.fn().mockResolvedValue({
name: 'Cypress',
version: '18.0.0',
}),
})
}
throw new Error('not found')
})
const versionsDataSource = new VersionsDataSource(ctx)
const versionInfo = await versionsDataSource.versionData()
expect(versionInfo).to.eql({
expect(versionInfo).toEqual({
current: {
id: currentCypressVersion,
version: currentCypressVersion,
@@ -107,97 +110,125 @@ describe('VersionsDataSource', () => {
ctx.coreData.machineId = Promise.resolve(null)
ctx.coreData.currentTestingType = 'component'
const mockRequest = {
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
'x-arch': 'x64',
'x-initial-launch': String(true),
'x-testing-type': 'component',
'x-logged-in': 'false',
}
fetchMock.mockImplementation((url: string, options: { headers: Record<string, string> }) => {
if (url === NPM_CYPRESS_REGISTRY_URL) {
return Promise.resolve({
// @ts-expect-error
json: jest.fn().mockResolvedValue({
'time': {
modified: '2022-01-31T21:14:41.593Z',
created: '2014-03-09T01:07:35.219Z',
[currentCypressVersion]: '2014-03-09T01:07:37.369Z',
'18.0.0': '2015-05-07T00:09:41.109Z',
},
}),
})
}
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: sinon.match(mockRequest),
}).resolves({
json: sinon.stub().resolves({
name: 'Cypress',
version: '15.0.0',
}),
})
// first mocked response
if (
url === CYPRESS_REMOTE_MANIFEST_URL &&
options.headers['Content-Type'] === 'application/json' &&
options.headers['x-cypress-version'] === currentCypressVersion &&
options.headers['x-os-name'] === 'darwin' &&
options.headers['x-arch'] === 'x64' &&
options.headers['x-initial-launch'] === String(true) &&
options.headers['x-testing-type'] === 'component' &&
options.headers['x-logged-in'] === 'false') {
return Promise.resolve({
// @ts-expect-error
json: jest.fn().mockResolvedValue({
name: 'Cypress',
version: '15.0.0',
}),
})
}
const mockRequest2 = {
...mockRequest,
'x-initial-launch': String(false),
'x-testing-type': 'e2e',
}
// second mocked response
if (
url === CYPRESS_REMOTE_MANIFEST_URL &&
options.headers['Content-Type'] === 'application/json' &&
options.headers['x-cypress-version'] === currentCypressVersion &&
options.headers['x-os-name'] === 'darwin' &&
options.headers['x-arch'] === 'x64' &&
options.headers['x-initial-launch'] === String(false) &&
options.headers['x-testing-type'] === 'e2e' &&
options.headers['x-logged-in'] === 'false' &&
options.headers['x-initial-launch'] === String(false)) {
return Promise.resolve({
// @ts-expect-error
json: jest.fn().mockResolvedValue({
name: 'Cypress',
version: '16.0.0',
}),
})
}
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: sinon.match(mockRequest2),
}).resolves({
json: sinon.stub().resolves({
name: 'Cypress',
version: '16.0.0',
}),
throw new Error('not found')
})
const versionsDataSource = new VersionsDataSource(ctx)
await versionsDataSource.versionData()
expect(await ctx.coreData.versionData?.latestVersion).to.eql('15.0.0')
expect(await ctx.coreData.versionData?.latestVersion).toEqual('15.0.0')
ctx.coreData.currentTestingType = 'e2e'
versionsDataSource.resetLatestVersionTelemetry()
expect(await ctx.coreData.versionData?.latestVersion).to.eql('16.0.0')
expect(await ctx.coreData.versionData?.latestVersion).toEqual('16.0.0')
})
it('handles errors fetching version data', async () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
'x-arch': 'x64',
'x-initial-launch': String(true),
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
}),
fetchMock.mockImplementation((url: string, options: { headers: Record<string, string> }) => {
if (url === NPM_CYPRESS_REGISTRY_URL) {
return Promise.reject(new Error('NPM_CYPRESS_REGISTRY_URL mocked response failed'))
}
if (
url === CYPRESS_REMOTE_MANIFEST_URL &&
options.headers['Content-Type'] === 'application/json' &&
options.headers['x-cypress-version'] === currentCypressVersion &&
options.headers['x-os-name'] === 'darwin' &&
options.headers['x-arch'] === 'x64' &&
options.headers['x-initial-launch'] === String(true) &&
options.headers['x-testing-type'] === 'e2e' &&
options.headers['x-logged-in'] === 'false') {
return Promise.reject(new Error('CYPRESS_REMOTE_MANIFEST_URL mocked response failed'))
}
throw new Error('not found')
})
.rejects()
.withArgs(NPM_CYPRESS_REGISTRY_URL)
.rejects()
const versionsDataSource = new VersionsDataSource(ctx)
const versionInfo = await versionsDataSource.versionData()
expect(versionInfo.current.version).to.eql(currentCypressVersion)
expect(versionInfo.current.version).toEqual(currentCypressVersion)
})
it('handles invalid response errors', async () => {
fetchStub
.withArgs(CYPRESS_REMOTE_MANIFEST_URL, {
headers: sinon.match({
'Content-Type': 'application/json',
'x-cypress-version': currentCypressVersion,
'x-os-name': 'darwin',
'x-arch': 'x64',
'x-initial-launch': String(true),
'x-machine-id': 'abcd123',
'x-testing-type': 'e2e',
'x-logged-in': 'false',
}),
fetchMock.mockImplementation((url: string, options: { headers: Record<string, string> }) => {
if (url === NPM_CYPRESS_REGISTRY_URL) {
return Promise.reject(new Response('Error'))
}
if (
url === CYPRESS_REMOTE_MANIFEST_URL &&
options.headers['Content-Type'] === 'application/json' &&
options.headers['x-cypress-version'] === currentCypressVersion &&
options.headers['x-os-name'] === 'darwin' &&
options.headers['x-arch'] === 'x64' &&
options.headers['x-initial-launch'] === String(true) &&
options.headers['x-machine-id'] === 'abcd123' &&
options.headers['x-testing-type'] === 'e2e' &&
options.headers['x-logged-in'] === 'false') {
return Promise.reject(new Response('Error'))
}
throw new Error('not found')
})
.callsFake(async () => new Response('Error'))
.withArgs(NPM_CYPRESS_REGISTRY_URL)
.callsFake(async () => new Response('Error'))
const versionsDataSource = new VersionsDataSource(ctx)
@@ -211,17 +242,17 @@ describe('VersionsDataSource', () => {
await ctx.coreData.versionData?.latestVersion
expect(versionInfo.current.version).to.eql(currentCypressVersion)
expect(versionInfo.current.version).toEqual(currentCypressVersion)
})
it('generates x-framework, x-bundler, and x-dependencies headers', async () => {
isDependencyInstalledByNameStub.callsFake(async (packageName) => {
isDependencyInstalledByNameStub.mockImplementation(async (packageName) => {
// Should include any resolved dependency with a valid version
if (packageName === 'react') {
return {
dependency: packageName,
detectedVersion: '1.2.3',
} as Cypress.DependencyToInstall
} as unknown as Cypress.DependencyToInstall
}
if (packageName === 'vue') {
@@ -264,10 +295,10 @@ describe('VersionsDataSource', () => {
versionsDataSource.resetLatestVersionTelemetry()
await versionsDataSource.versionData()
expect(fetchStub).to.have.been.calledWith(
expect(fetchMock).toHaveBeenCalledWith(
CYPRESS_REMOTE_MANIFEST_URL,
{
headers: sinon.match({
headers: expect.objectContaining({
'x-framework': 'react',
'x-dev-server': 'vite',
'x-dependencies': 'react@1.2.3,vue@4.5.6,@builder.io/qwik@1.1.4,@playwright/experimental-ct-core@1.33.0',
@@ -277,22 +308,22 @@ describe('VersionsDataSource', () => {
})
it('generates x-notifications header', async () => {
(ctx.config.localSettingsApi.getPreferences as sinon.SinonStub).callsFake(() => {
return {
(ctx.config.localSettingsApi.getPreferences as jest.Mock<typeof ctx.config.localSettingsApi.getPreferences>).mockImplementation(() => {
return Promise.resolve({
notifyWhenRunCompletes: ['errored'],
notifyWhenRunStarts: true,
notifyWhenRunStartsFailing: true,
}
}as unknown as AllowedState)
})
const versionsDataSource = new VersionsDataSource(ctx)
await versionsDataSource.versionData()
expect(fetchStub).to.have.been.calledWith(
expect(fetchMock).toHaveBeenCalledWith(
CYPRESS_REMOTE_MANIFEST_URL,
{
headers: sinon.match({
headers: expect.objectContaining({
'x-notifications': 'errored,started,failing',
}),
},
@@ -1,5 +1,5 @@
import { describe, expect, it, beforeAll } from '@jest/globals'
import { WizardBundler, WIZARD_BUNDLERS, CT_FRAMEWORKS, resolveComponentFrameworkDefinition } from '@packages/scaffold-config'
import { expect } from 'chai'
import { createTestDataContext, scaffoldMigrationProject, removeCommonNodeModules } from '../helper'
function findFramework (type: Cypress.ResolvedComponentFrameworkDefinition['type']) {
@@ -11,7 +11,7 @@ function findBundler (type: WizardBundler['type']) {
}
describe('packagesToInstall', () => {
before(() => {
beforeAll(() => {
removeCommonNodeModules()
})
@@ -28,7 +28,7 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D webpack react react-dom`)
expect(actual).toEqual(`npm install -D webpack react react-dom`)
})
it('regular vue project with webpack', async () => {
@@ -44,7 +44,7 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D webpack vue`)
expect(actual).toEqual(`npm install -D webpack vue`)
})
it('regular react project with vite', async () => {
@@ -60,7 +60,7 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D vite react react-dom`)
expect(actual).toEqual(`npm install -D vite react react-dom`)
})
it('regular vue project with vite', async () => {
@@ -76,7 +76,7 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D vite vue`)
expect(actual).toEqual(`npm install -D vite vue`)
})
it('nextjs-unconfigured', async () => {
@@ -92,7 +92,7 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq(`npm install -D next react react-dom`)
expect(actual).toEqual(`npm install -D next react react-dom`)
})
it('framework and bundler are undefined', async () => {
@@ -108,6 +108,6 @@ describe('packagesToInstall', () => {
const actual = await ctx.wizard.installDependenciesCommand()
expect(actual).to.eq('')
expect(actual).toEqual('')
})
})
@@ -1,5 +1,5 @@
import { describe, expect, it } from '@jest/globals'
import { graphqlSchema } from '../../../graphql/schema'
import { expect } from 'chai'
import dedent from 'dedent'
import { FieldNode, GraphQLObjectType, OperationDefinitionNode, parse, print } from 'graphql'
import { DocumentNodeBuilder } from '../../../src'
@@ -41,7 +41,7 @@ describe('DocumentNodeBuilder', () => {
operationName: 'CLOUD_VIEWER_QUERY',
})
expect(print(docNodeBuilder.frag)).to.eql(dedent`
expect(print(docNodeBuilder.frag)).toEqual(dedent`
fragment GeneratedFragment on Query {
cloudViewer {
id
@@ -60,7 +60,7 @@ describe('DocumentNodeBuilder', () => {
operationName: 'CLOUD_VIEWER_QUERY',
})
expect(print(docNodeBuilder.query).trimEnd()).to.eql(dedent`
expect(print(docNodeBuilder.query).trimEnd()).toEqual(dedent`
fragment GeneratedFragment on Query {
cloudViewer {
id
@@ -86,7 +86,7 @@ describe('DocumentNodeBuilder', () => {
operationName: 'CLOUD_PROJECT_QUERY',
})
expect(print(docNodeBuilder.queryNode).trimRight()).to.eql(dedent`
expect(print(docNodeBuilder.queryNode).trimRight()).toEqual(dedent`
fragment GeneratedFragment on CloudProject {
id
cloudProject {
@@ -1,4 +1,4 @@
import { expect } from 'chai'
import { describe, expect, it } from '@jest/globals'
import path from 'path'
import { hasTypeScriptInstalled } from '../../../src/util'
import { scaffoldMigrationProject } from '../helper'
@@ -7,12 +7,12 @@ describe('hasTypeScript', () => {
it('returns true when installed', async () => {
const monorepoRoot = path.join(__dirname, '..', '..', '..', '..', '..')
expect(hasTypeScriptInstalled(monorepoRoot)).to.be.true
expect(hasTypeScriptInstalled(monorepoRoot)).toBe(true)
})
it('returns false when not installed', async () => {
const projectRoot = await scaffoldMigrationProject('config-with-js')
expect(hasTypeScriptInstalled(projectRoot)).to.be.false
expect(hasTypeScriptInstalled(projectRoot)).toBe(false)
})
})
@@ -1,6 +1,5 @@
import { describe, expect, it } from '@jest/globals'
import { SpecWithRelativeRoot } from '@packages/types'
import { expect } from 'chai'
import fs from 'fs-extra'
import { scaffoldMigrationProject } from '../helper'
import { getTestCounts } from '../../../src/util/testCounts'
@@ -12,7 +11,7 @@ describe('getTestCounts', () => {
const counts = await getTestCounts(specs)
expect(counts).to.deep.equal({
expect(counts).toEqual({
totalSpecs: 0,
totalTests: 0,
exampleSpecs: 0,
@@ -20,7 +19,7 @@ describe('getTestCounts', () => {
})
})
context('with e2e project', () => {
describe('with e2e project', () => {
let specs: SpecWithRelativeRoot[]
beforeEach(async () => {
@@ -44,11 +43,11 @@ describe('getTestCounts', () => {
it('should return counts for tests e2e migration project', async () => {
const counts = await getTestCounts(specs)
expect(counts.totalSpecs).to.equal(specs.length)
expect(counts.totalSpecs).toEqual(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)
expect(counts.totalTests).toBeGreaterThan(0)
expect(counts.exampleSpecs).toEqual(0)
expect(counts.exampleTests).toEqual(0)
})
})
})
@@ -1,9 +1,9 @@
import { expect } from 'chai'
import { describe, expect, it } from '@jest/globals'
import { WEIGHTED, WEIGHTED_EVEN } from '../../../src/util/weightedChoice'
describe('weightedChoice', () => {
context('WeightedAlgorithm', () => {
describe('WeightedAlgorithm', () => {
it('should error if invalid arguments', () => {
const weights = [25, 75, 45]
const options = ['A', 'B']
@@ -12,7 +12,7 @@ describe('weightedChoice', () => {
WEIGHTED(weights).pick(options)
}
expect(func).to.throw()
expect(func).toThrow()
})
it('should error if weights is empty', () => {
@@ -23,7 +23,7 @@ describe('weightedChoice', () => {
WEIGHTED(weights).pick(options)
}
expect(func).to.throw()
expect(func).toThrow()
})
it('should error if options is empty', () => {
@@ -34,7 +34,7 @@ describe('weightedChoice', () => {
WEIGHTED(weights).pick(options)
}
expect(func).to.throw()
expect(func).toThrow()
})
it('should return an option', () => {
@@ -42,20 +42,20 @@ describe('weightedChoice', () => {
const options = ['A', 'B']
const selected = WEIGHTED(weights).pick(options)
expect(options.includes(selected)).to.be.true
expect(options.includes(selected)).toBe(true)
})
})
context('WEIGHTED_EVEN', () => {
describe('WEIGHTED_EVEN', () => {
it('should return an option', () => {
const options = ['A', 'B']
const selected = WEIGHTED_EVEN(options).pick(options)
expect(options.includes(selected)).to.be.true
expect(options.includes(selected)).toBe(true)
})
})
context('randomness', () => {
describe('randomness', () => {
it('should return values close to supplied weights', () => {
const results = {}
const options = ['A', 'B']
@@ -68,7 +68,7 @@ describe('weightedChoice', () => {
}
Object.keys(results).forEach((key) => {
expect(Math.round(results[key] / 100)).to.equal(5)
expect(Math.round(results[key] / 100)).toEqual(5)
})
})
})
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"isolatedModules": true
}
}
@@ -0,0 +1,126 @@
// necessary to have mocha types working correctly
// NOTE: this is the sinon version of @packages/data-context/test/unit/helper.ts and will eventually be replaced with the a different version
import 'mocha'
import path from 'path'
import fs from 'fs-extra'
import { Response } from 'cross-fetch'
import Fixtures, { fixtureDirs, scaffoldProject, removeProject } from '@tooling/system-tests'
import { DataContext, DataContextConfig } from '@packages/data-context/src'
import { graphqlSchema } from '@packages/data-context/graphql/schema'
import { remoteSchemaWrapped as schemaCloud } from '@packages/data-context/graphql/stitching/remoteSchemaWrapped'
import type { BrowserApiShape } from '@packages/data-context/src/sources/BrowserDataSource'
import type { AppApiShape, AuthApiShape, ElectronApiShape, LocalSettingsApiShape, ProjectApiShape, CohortsApiShape } from '@packages/data-context/src/actions'
import sinon from 'sinon'
import { execute, parse } from 'graphql'
import { getOperationName } from '@urql/core'
import { CloudQuery } from '@packages/data-context/test/graphql/stubCloudTypes'
import { remoteSchema } from '@packages/data-context/graphql/stitching/remoteSchema'
import type { OpenModeOptions, RunModeOptions } from '@packages/types'
import { GET_MAJOR_VERSION_FOR_CONTENT } from '@packages/types'
import { RelevantRunInfo } from '@packages/data-context/src/gen/graphcache-config.gen'
type SystemTestProject = typeof fixtureDirs[number]
type SystemTestProjectPath<T extends SystemTestProject> = `${string}/system-tests/projects/${T}`
export { scaffoldProject, removeProject }
export function getSystemTestProject<T extends typeof fixtureDirs[number]> (project: T): SystemTestProjectPath<T> {
return path.join(__dirname, '..', '..', '..', '..', 'system-tests', 'projects', project) as SystemTestProjectPath<T>
}
export function removeCommonNodeModules () {
fs.rmSync(path.join(Fixtures.cyTmpDir, 'node_modules'), { recursive: true, force: true })
}
export async function scaffoldMigrationProject (project: typeof fixtureDirs[number]): Promise<string> {
Fixtures.removeProject(project)
await Fixtures.scaffoldProject(project)
return Fixtures.projectPath(project)
}
export function createTestDataContext (mode: DataContextConfig['mode'] = 'run', modeOptions: Partial<RunModeOptions | OpenModeOptions> = {}) {
const ctx = new DataContext({
schema: graphqlSchema,
schemaCloud,
mode,
modeOptions,
appApi: {} as AppApiShape,
localSettingsApi: {
getPreferences: sinon.stub().resolves({
majorVersionWelcomeDismissed: { [GET_MAJOR_VERSION_FOR_CONTENT()]: 123456 },
notifyWhenRunCompletes: ['failed'],
}),
getAvailableEditors: sinon.stub(),
setPreferences: sinon.stub(),
} as unknown as LocalSettingsApiShape,
authApi: {
logIn: sinon.stub().throws('not stubbed'),
resetAuthState: sinon.stub(),
} as unknown as AuthApiShape,
projectApi: {
closeActiveProject: sinon.stub(),
insertProjectToCache: sinon.stub().resolves(),
getProjectRootsFromCache: sinon.stub().resolves([]),
runSpec: sinon.stub(),
routeToDebug: sinon.stub(),
} as unknown as ProjectApiShape,
electronApi: {
isMainWindowFocused: sinon.stub().returns(false),
focusMainWindow: sinon.stub(),
copyTextToClipboard: (text) => {},
} as unknown as ElectronApiShape,
browserApi: {
focusActiveBrowserWindow: sinon.stub(),
getBrowsers: sinon.stub().resolves([]),
} as unknown as BrowserApiShape,
cohortsApi: {
getCohorts: sinon.stub().resolves(),
getCohort: sinon.stub().resolves(),
insertCohort: sinon.stub(),
determineCohort: sinon.stub().resolves(),
} as unknown as CohortsApiShape,
})
const origFetch = ctx.util.fetch
ctx.util.fetch = async function (url, init) {
await new Promise((resolve) => setTimeout(resolve, 5))
if (String(url).endsWith('/test-runner-graphql')) {
const { query, variables } = JSON.parse(String(init?.body))
const document = parse(query)
const operationName = getOperationName(document)
const result = await Promise.resolve(execute({
operationName,
variableValues: variables,
rootValue: CloudQuery,
contextValue: {
__server__: ctx,
},
schema: remoteSchema,
document,
}))
return new Response(JSON.stringify(result), { status: 200 })
}
return origFetch.call(this, url, init)
}
return ctx
}
export function createRelevantRun (runNumber: number): RelevantRunInfo {
return {
runNumber,
ciBuildNumber: '123',
branch: 'feature/branch',
organizationId: 'org-id',
sha: 'sha-123',
totalFailed: 0,
}
}
@@ -9,7 +9,7 @@ import snapshot from 'snap-shot-it'
import { EventEmitter } from 'events'
import { exec } from 'child_process'
import util from 'util'
import { createTestDataContext } from '@packages/data-context/test/unit/helper'
import { createTestDataContext } from '../../support/helpers/data-context-helper'
import electron from '../../../lib/browsers/electron'
import chrome from '../../../lib/browsers/chrome'
import Promise from 'bluebird'
+1132 -109
View File
File diff suppressed because it is too large Load Diff