mirror of
https://github.com/cypress-io/cypress.git
synced 2026-03-13 12:59:07 -05:00
Merge branch '10.0-release' of github.com:cypress-io/cypress into md-10.0-merge
This commit is contained in:
@@ -74,6 +74,8 @@ npm/cypress-schematic/src/**/*.js
|
||||
/npm/vue2/**/*.vue
|
||||
|
||||
packages/data-context/test/unit/codegen/files
|
||||
packages/config/test/__fixtures__/**/*
|
||||
packages/config/test/__babel_fixtures__/**/*
|
||||
|
||||
# community templates we test against, no need to lint
|
||||
system-tests/projects/cra-4/**/*
|
||||
|
||||
24
circle.yml
24
circle.yml
@@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters
|
||||
only:
|
||||
- develop
|
||||
- 10.0-release
|
||||
- chore/cutover-to-bundled-react-mount
|
||||
- new-cmd-log-styles
|
||||
|
||||
# uncomment & add to the branch conditions below to disable the main linux
|
||||
# flow if we don't want to test it for a certain branch
|
||||
@@ -49,9 +49,7 @@ macWorkflowFilters: &mac-workflow-filters
|
||||
or:
|
||||
- equal: [ develop, << pipeline.git.branch >> ]
|
||||
- equal: [ '10.0-release', << pipeline.git.branch >> ]
|
||||
- equal: [ chore/cutover-to-bundled-react-mount, << pipeline.git.branch >> ]
|
||||
- equal: [ feature-multidomain, << pipeline.git.branch >> ]
|
||||
- equal: [ unify-1449-beta-slug-length, << pipeline.git.branch >> ]
|
||||
- equal: [ new-cmd-log-styles, << pipeline.git.branch >> ]
|
||||
- matches:
|
||||
pattern: "-release$"
|
||||
value: << pipeline.git.branch >>
|
||||
@@ -114,6 +112,16 @@ executors:
|
||||
PLATFORM: windows
|
||||
|
||||
commands:
|
||||
verify_should_persist_artifacts:
|
||||
steps:
|
||||
- run:
|
||||
name: Check current branch to persist artifacts
|
||||
command: |
|
||||
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "new-cmd-log-styles" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then
|
||||
echo "Not uploading artifacts or posting install comment for this branch."
|
||||
circleci-agent step halt
|
||||
fi
|
||||
|
||||
restore_workspace_binaries:
|
||||
steps:
|
||||
- attach_workspace:
|
||||
@@ -1773,13 +1781,7 @@ jobs:
|
||||
- build-binary
|
||||
- build-cypress-npm-package:
|
||||
executor: << parameters.executor >>
|
||||
- run:
|
||||
name: Check current branch to persist artifacts
|
||||
command: |
|
||||
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "chore/cutover-to-bundled-react-mount" && "$CIRCLE_BRANCH" != "unify-1036-windows-test-projects" && "$CIRCLE_BRANCH" != "10.0-release" && "$CIRCLE_BRANCH" != "unify-1449-beta-slug-length" && "$CIRCLE_BRANCH" != "feature-multidomain" ]]; then
|
||||
echo "Not uploading artifacts or posting install comment for this branch."
|
||||
circleci-agent step halt
|
||||
fi
|
||||
- verify_should_persist_artifacts
|
||||
- upload-build-artifacts
|
||||
- post-install-comment
|
||||
|
||||
|
||||
2
cli/types/cypress.d.ts
vendored
2
cli/types/cypress.d.ts
vendored
@@ -2983,7 +2983,7 @@ declare namespace Cypress {
|
||||
xhrUrl: string
|
||||
}
|
||||
|
||||
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations'>> {
|
||||
interface TestConfigOverrides extends Partial<Pick<ConfigOptions, 'animationDistanceThreshold' | 'blockHosts' | 'defaultCommandTimeout' | 'env' | 'execTimeout' | 'includeShadowDom' | 'numTestsKeptInMemory' | 'pageLoadTimeout' | 'redirectionLimit' | 'requestTimeout' | 'responseTimeout' | 'retries' | 'screenshotOnRunFailure' | 'slowTestThreshold' | 'scrollBehavior' | 'taskTimeout' | 'viewportHeight' | 'viewportWidth' | 'waitForAnimations' | 'experimentalSessionSupport'>> {
|
||||
browser?: IsBrowserMatcher | IsBrowserMatcher[]
|
||||
keystrokeDelay?: number
|
||||
}
|
||||
|
||||
@@ -13,13 +13,15 @@ const supportFile = CypressInstance.config('supportFile')
|
||||
const projectRoot = CypressInstance.config('projectRoot')
|
||||
const devServerPublicPathRoute = CypressInstance.config('devServerPublicPathRoute')
|
||||
|
||||
let supportRelativeToProjectRoot = supportFile.replace(projectRoot, '')
|
||||
|
||||
if (CypressInstance.config('platform') === 'win32') {
|
||||
supportRelativeToProjectRoot = supportFile.replace(projectRoot.replaceAll('/', '\\'))
|
||||
}
|
||||
|
||||
if (supportFile) {
|
||||
let supportRelativeToProjectRoot = supportFile.replace(projectRoot, '')
|
||||
|
||||
if (CypressInstance.config('platform') === 'win32') {
|
||||
const platformProjectRoot = projectRoot.replaceAll('/', '\\')
|
||||
|
||||
supportRelativeToProjectRoot = supportFile.replace(platformProjectRoot, '')
|
||||
}
|
||||
|
||||
// We need a slash before /cypress/supportFile.js, this happens by default
|
||||
// with the current string replacement logic.
|
||||
importsToLoad.push(() => import(`${devServerPublicPathRoute}${supportRelativeToProjectRoot}`))
|
||||
|
||||
11
npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
Normal file
11
npm/vite-dev-server/cypress/e2e/vite-dev-server.cy.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
describe('Config options', () => {
|
||||
it('supports supportFile = false', () => {
|
||||
cy.scaffoldProject('vite2.9.1-react')
|
||||
cy.openProject('vite2.9.1-react', ['--config-file', 'cypress-vite-no-support.config.ts'])
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.visitApp()
|
||||
cy.contains('App.cy.jsx').click()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
})
|
||||
})
|
||||
11
npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
Normal file
11
npm/webpack-dev-server/cypress/e2e/webpack-dev-server.cy.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
describe('Config options', () => {
|
||||
it('supports supportFile = false', () => {
|
||||
cy.scaffoldProject('webpack5_wds4-react')
|
||||
cy.openProject('webpack5_wds4-react', ['--config-file', 'cypress-webpack-no-support.config.ts'])
|
||||
cy.startAppServer('component')
|
||||
|
||||
cy.visitApp()
|
||||
cy.contains('App.cy.jsx').click()
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
})
|
||||
})
|
||||
@@ -270,7 +270,7 @@ describe('src/cypress/runner', () => {
|
||||
failCount: 1,
|
||||
})
|
||||
|
||||
cy.get('.command-number:contains(25)').should('be.visible')
|
||||
cy.get('.command-number-column:contains(25)').should('be.visible')
|
||||
})
|
||||
|
||||
it('file with empty suites only displays no tests found', () => {
|
||||
|
||||
@@ -77,6 +77,32 @@ describe('App: Settings', () => {
|
||||
expect((ctx.actions.electron.openExternal as SinonStub).lastCall.lastArg).to.eq('http:/test.cloud/cloud-project/settings')
|
||||
})
|
||||
})
|
||||
|
||||
it('shows deferred remote cloud data after navigating from a run', { retries: 0 }, () => {
|
||||
cy.remoteGraphQLIntercept(async (obj) => {
|
||||
// Simulate a timeout so we don't resolve immediately, previously visiting the test runner
|
||||
// and then leaving would cause this to fail, because it removed the event listeners
|
||||
// for graphql-refetch. By namespacing the socket layer, we avoid the events of the
|
||||
// runner from impacting the cloud behavior
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
return obj.result
|
||||
})
|
||||
|
||||
cy.startAppServer('e2e')
|
||||
cy.loginUser()
|
||||
cy.visitApp()
|
||||
cy.get('.spec-list-container').scrollTo('bottom')
|
||||
// Visit the test to trigger the ws.off() for the TR websockets
|
||||
cy.contains('test1.js').click()
|
||||
// Wait for the test to pass, so the test is completed
|
||||
cy.get('.passed > .num').should('contain', 1)
|
||||
cy.get(`[href='#/settings']`).click()
|
||||
cy.contains('Dashboard Settings').click()
|
||||
// Assert the data is not there before it arrives
|
||||
cy.contains('Record Key').should('not.exist')
|
||||
cy.contains('Record Key')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Project Settings', () => {
|
||||
|
||||
@@ -28,6 +28,7 @@ describe('Sidebar Navigation', () => {
|
||||
cy.openProject('todos')
|
||||
cy.startAppServer()
|
||||
cy.visitApp()
|
||||
cy.contains('todos')
|
||||
})
|
||||
|
||||
it('expands the left nav bar by default', () => {
|
||||
@@ -50,7 +51,6 @@ describe('Sidebar Navigation', () => {
|
||||
|
||||
it('closes the left nav bar when clicking the expand button (if expanded)', () => {
|
||||
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
|
||||
cy.contains('todos')
|
||||
cy.findAllByText('todos').eq(1).as('title')
|
||||
cy.get('@title').should('be.visible')
|
||||
|
||||
@@ -65,7 +65,6 @@ describe('Sidebar Navigation', () => {
|
||||
|
||||
it('closes the left nav bar when clicking the expand button and persist the state if browser is refreshed', () => {
|
||||
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
|
||||
cy.contains('todos')
|
||||
cy.findAllByText('todos').eq(1).as('title')
|
||||
cy.get('@title').should('be.visible')
|
||||
|
||||
@@ -258,13 +257,13 @@ describe('Sidebar Navigation', () => {
|
||||
o.sinon.stub(ctx.actions.localSettings, 'setPreferences').resolves()
|
||||
})
|
||||
|
||||
cy.get('[data-cy="reporter-panel"]').invoke('outerWidth').then(($initialWidth) => {
|
||||
cy.get('[data-cy="panel2ResizeHandle"]').trigger('mousedown', { eventConstructor: 'MouseEvent' })
|
||||
.trigger('mousemove', { clientX: 400 })
|
||||
.trigger('mouseup', { eventConstructor: 'MouseEvent' })
|
||||
})
|
||||
cy.get('[data-cy="reporter-panel"]').invoke('outerWidth').should('eq', 450)
|
||||
|
||||
cy.withCtx((ctx, o) => {
|
||||
cy.get('[data-cy="panel2ResizeHandle"]').trigger('mousedown', { eventConstructor: 'MouseEvent' })
|
||||
.trigger('mousemove', { clientX: 400 })
|
||||
.trigger('mouseup', { eventConstructor: 'MouseEvent' })
|
||||
|
||||
cy.withRetryableCtx((ctx, o) => {
|
||||
expect((ctx.actions.localSettings.setPreferences as SinonStub).lastCall.lastArg).to.eq('{"reporterWidth":336}')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,8 +6,8 @@ describe('App: Specs', () => {
|
||||
describe('Testing Type: E2E', () => {
|
||||
context('js project with default spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
cy.scaffoldProject('no-specs')
|
||||
cy.openProject('no-specs')
|
||||
cy.startAppServer('e2e')
|
||||
cy.visitApp()
|
||||
|
||||
@@ -193,14 +193,14 @@ describe('App: Specs', () => {
|
||||
|
||||
context('ts project with default spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
cy.scaffoldProject('no-specs')
|
||||
cy.openProject('no-specs')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('tsconfig.json', '{}')
|
||||
})
|
||||
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs')
|
||||
|
||||
cy.startAppServer('e2e')
|
||||
cy.visitApp()
|
||||
@@ -469,10 +469,10 @@ describe('App: Specs', () => {
|
||||
viewportHeight: 768,
|
||||
viewportWidth: 1024,
|
||||
}, () => {
|
||||
context('project without storybook', () => {
|
||||
context('project with default spec pattern', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('no-specs-no-storybook')
|
||||
cy.openProject('no-specs-no-storybook')
|
||||
cy.scaffoldProject('no-specs')
|
||||
cy.openProject('no-specs')
|
||||
cy.startAppServer('component')
|
||||
cy.visitApp()
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
function updateProjectIdInCypressConfig (value: string) {
|
||||
return cy.withCtx((ctx, o) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`projectId: 'abc123'`, `projectId: '${o.value}'`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
}, { value })
|
||||
}
|
||||
|
||||
function updateViewportHeightInCypressConfig (value: number) {
|
||||
return cy.withCtx((ctx, o) => {
|
||||
let config = ctx.actions.file.readFileInProject('cypress.config.js')
|
||||
|
||||
config = config.replace(`e2e: {`, `e2e: {\n viewportHeight: ${o.value},\n`)
|
||||
ctx.actions.file.writeFileInProject('cypress.config.js', config)
|
||||
}, { value })
|
||||
}
|
||||
|
||||
describe('specChange subscription', () => {
|
||||
beforeEach(() => {
|
||||
cy.scaffoldProject('cypress-in-cypress')
|
||||
cy.openProject('cypress-in-cypress')
|
||||
cy.startAppServer()
|
||||
cy.visitApp()
|
||||
})
|
||||
|
||||
describe('on config page', () => {
|
||||
it('responds to configChange event when viewport is changed', () => {
|
||||
cy.get('a').contains('Settings').click()
|
||||
cy.get('[data-cy="collapsible-header"]').contains('Project Settings').click()
|
||||
cy.contains(`projectId: 'abc123'`)
|
||||
updateProjectIdInCypressConfig('foo456')
|
||||
cy.contains(`projectId: 'foo456'`)
|
||||
})
|
||||
})
|
||||
|
||||
describe('on runner page', () => {
|
||||
it('responds to configChange event and re-runs spec', () => {
|
||||
// run spec
|
||||
cy.contains('dom-content.spec').click()
|
||||
|
||||
// wait until it has passed
|
||||
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
|
||||
cy.get('button').contains('1000x660')
|
||||
|
||||
// update the config - the spec should re-execute with the new viewportHeight
|
||||
updateViewportHeightInCypressConfig(777)
|
||||
|
||||
cy.get('[data-model-state="passed"]').should('contain', 'renders the test content')
|
||||
cy.get('button').contains('1000x777')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -22,8 +22,8 @@ describe('specChange subscription', () => {
|
||||
.should('contain', 'dom-content.spec.js')
|
||||
.should('contain', 'dom-list.spec.js')
|
||||
|
||||
cy.withCtx(async (ctx, o) => {
|
||||
await ctx.actions.file.writeFileInProject(o.path, '')
|
||||
cy.withCtx((ctx, o) => {
|
||||
ctx.actions.file.writeFileInProject(o.path, '')
|
||||
}, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') })
|
||||
|
||||
cy.get('[data-cy="spec-item-link"]')
|
||||
|
||||
@@ -6,15 +6,25 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useQuery } from '@urql/vue'
|
||||
import { gql, useQuery, useSubscription } from '@urql/vue'
|
||||
import SettingsContainer from '../settings/SettingsContainer.vue'
|
||||
import { SettingsDocument } from '../generated/graphql'
|
||||
import { Config_ConfigChangeDocument, SettingsDocument } from '../generated/graphql'
|
||||
|
||||
gql`
|
||||
query Settings {
|
||||
...SettingsContainer
|
||||
}`
|
||||
|
||||
gql`
|
||||
subscription Config_ConfigChange {
|
||||
configChange {
|
||||
id
|
||||
...ProjectSettings
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const query = useQuery({ query: SettingsDocument })
|
||||
|
||||
useSubscription({ query: Config_ConfigChangeDocument })
|
||||
</script>
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { gql, useQuery, useSubscription } from '@urql/vue'
|
||||
import { SpecPageContainerDocument, SpecPageContainer_SpecsChangeDocument } from '../../generated/graphql'
|
||||
import { gql, useQuery, useSubscription, SubscriptionHandlerArg } from '@urql/vue'
|
||||
import { SpecPageContainerDocument, SpecPageContainer_SpecsChangeDocument, Runner_ConfigChangeDocument, Runner_ConfigChangeSubscription } from '../../generated/graphql'
|
||||
import SpecRunnerContainerOpenMode from '../../runner/SpecRunnerContainerOpenMode.vue'
|
||||
import SpecRunnerContainerRunMode from '../../runner/SpecRunnerContainerRunMode.vue'
|
||||
import { useEventManager } from '../../runner/useEventManager'
|
||||
import { useSpecStore } from '../../store'
|
||||
|
||||
gql`
|
||||
query SpecPageContainer {
|
||||
@@ -48,10 +50,29 @@ subscription SpecPageContainer_specsChange {
|
||||
}
|
||||
`
|
||||
|
||||
useSubscription({ query: SpecPageContainer_SpecsChangeDocument })
|
||||
gql`
|
||||
subscription Runner_ConfigChange {
|
||||
configChange {
|
||||
id
|
||||
serveConfig
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const isRunMode = window.__CYPRESS_MODE__ === 'run'
|
||||
|
||||
// subscriptions are used to trigger live updates without
|
||||
// reloading the page.
|
||||
// this is only useful in open mode - in run mode, we don't
|
||||
// use GraphQL, so we pause the
|
||||
// subscriptions so they never execute.
|
||||
const shouldPauseSubscriptions = isRunMode && window.top === window
|
||||
|
||||
useSubscription({
|
||||
query: SpecPageContainer_SpecsChangeDocument,
|
||||
pause: shouldPauseSubscriptions,
|
||||
})
|
||||
|
||||
// in run mode, we are not using GraphQL or urql
|
||||
// for performance - run mode does not need the
|
||||
// same level of runner interactivity as open mode.
|
||||
@@ -59,9 +80,46 @@ const isRunMode = window.__CYPRESS_MODE__ === 'run'
|
||||
// requests, which is what we want.
|
||||
const query = useQuery({
|
||||
query: SpecPageContainerDocument,
|
||||
pause: isRunMode && window.top === window,
|
||||
pause: shouldPauseSubscriptions,
|
||||
})
|
||||
|
||||
let initialLoad = true
|
||||
|
||||
const specStore = useSpecStore()
|
||||
|
||||
// When cypress.config.js is changed,
|
||||
// we respond by updating the runner with the latest config
|
||||
// and re-running the current spec with the new config values.
|
||||
const configChangeHandler: SubscriptionHandlerArg<any, any> = (
|
||||
_prev: Runner_ConfigChangeSubscription | undefined,
|
||||
next: Runner_ConfigChangeSubscription,
|
||||
) => {
|
||||
if (!next.configChange?.serveConfig) {
|
||||
throw Error(`Did not get expected serveConfig from subscription`)
|
||||
}
|
||||
|
||||
if (!initialLoad && specStore.activeSpec) {
|
||||
try {
|
||||
// Update the config used by the runner with the new one.
|
||||
window.__CYPRESS_CONFIG__ = next.configChange.serveConfig
|
||||
|
||||
const eventManager = useEventManager()
|
||||
|
||||
eventManager.runSpec()
|
||||
} catch (e) {
|
||||
// eventManager may not be defined, for example if the spec
|
||||
// is still loading.
|
||||
// In that case, just do nothing - the spec will be executed soon.
|
||||
// This only happens when re-executing a spec after
|
||||
// cypress.config.js was changed.
|
||||
}
|
||||
}
|
||||
|
||||
initialLoad = false
|
||||
}
|
||||
|
||||
useSubscription({ query: Runner_ConfigChangeDocument }, configChangeHandler)
|
||||
|
||||
// because we are not using GraphQL in run mode, and we still need
|
||||
// way to get the specs, we simply attach them to window when
|
||||
// serving the initial HTML.
|
||||
|
||||
@@ -131,11 +131,19 @@ fragment SpecRunner_Preferences on Query {
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
fragment SpecRunner_Config on CurrentProject {
|
||||
id
|
||||
config
|
||||
}
|
||||
`
|
||||
|
||||
gql`
|
||||
fragment SpecRunner on Query {
|
||||
...Specs_InlineSpecList
|
||||
currentProject {
|
||||
id
|
||||
...SpecRunner_Config
|
||||
...SpecRunnerHeader
|
||||
...AutomationMissing
|
||||
}
|
||||
|
||||
@@ -121,16 +121,17 @@ function createIframeModel () {
|
||||
* for communication between driver, runner, reporter via event bus,
|
||||
* and server (via web socket).
|
||||
*/
|
||||
function setupRunner (namespace: AutomationElementId) {
|
||||
function setupRunner () {
|
||||
const mobxRunnerStore = getMobxRunnerStore()
|
||||
const runnerUiStore = useRunnerUiStore()
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
getEventManager().addGlobalListeners(mobxRunnerStore, {
|
||||
randomString: runnerUiStore.randomString,
|
||||
element: getAutomationElementId(),
|
||||
})
|
||||
|
||||
getEventManager().start(window.UnifiedRunner.config)
|
||||
getEventManager().start(config)
|
||||
|
||||
const autStore = useAutStore()
|
||||
|
||||
@@ -217,7 +218,7 @@ export function addCrossOriginIframe (location) {
|
||||
*/
|
||||
function runSpecCT (spec: SpecFile) {
|
||||
// TODO: UNIFY-1318 - figure out how to manage window.config.
|
||||
const config = window.UnifiedRunner.config
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
// this is how the Cypress driver knows which spec to run.
|
||||
config.spec = setSpecForDriver(spec)
|
||||
@@ -282,7 +283,7 @@ function setSpecForDriver (spec: SpecFile) {
|
||||
*/
|
||||
function runSpecE2E (spec: SpecFile) {
|
||||
// TODO: UNIFY-1318 - manage config with GraphQL, don't put it on window.
|
||||
const config = window.UnifiedRunner.config
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
// this is how the Cypress driver knows which spec to run.
|
||||
config.spec = setSpecForDriver(spec)
|
||||
@@ -338,6 +339,10 @@ function runSpecE2E (spec: SpecFile) {
|
||||
getEventManager().initialize($autIframe, config)
|
||||
}
|
||||
|
||||
function getRunnerConfigFromWindow () {
|
||||
return JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config))
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject the global `UnifiedRunner` via a <script src="..."> tag.
|
||||
* which includes the event manager and AutIframe constructor.
|
||||
@@ -350,7 +355,7 @@ async function initialize () {
|
||||
|
||||
isTorndown = false
|
||||
|
||||
const config = JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config))
|
||||
const config = getRunnerConfigFromWindow()
|
||||
|
||||
if (isTorndown) {
|
||||
return
|
||||
@@ -363,21 +368,17 @@ async function initialize () {
|
||||
// find out if we need to continue managing viewportWidth/viewportHeight in MobX at all.
|
||||
autStore.updateDimensions(config.viewportWidth, config.viewportHeight)
|
||||
|
||||
// just stick config on window until we figure out how we are
|
||||
// going to manage it
|
||||
window.UnifiedRunner.config = config
|
||||
|
||||
// window.UnifiedRunner exists now, since the Webpack bundle with
|
||||
// the UnifiedRunner namespace was injected by `injectBundle`.
|
||||
initializeEventManager(window.UnifiedRunner)
|
||||
|
||||
window.UnifiedRunner.MobX.runInAction(() => {
|
||||
const store = initializeMobxStore(window.UnifiedRunner.config.testingType)
|
||||
const store = initializeMobxStore(window.__CYPRESS_TESTING_TYPE__)
|
||||
|
||||
store.updateDimensions(config.viewportWidth, config.viewportHeight)
|
||||
})
|
||||
|
||||
window.UnifiedRunner.MobX.runInAction(() => setupRunner(config.namespace))
|
||||
window.UnifiedRunner.MobX.runInAction(() => setupRunner())
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,15 +411,15 @@ async function executeSpec (spec: SpecFile) {
|
||||
|
||||
UnifiedReporterAPI.setupReporter()
|
||||
|
||||
if (window.UnifiedRunner.config.testingType === 'e2e') {
|
||||
if (window.__CYPRESS_TESTING_TYPE__ === 'e2e') {
|
||||
return runSpecE2E(spec)
|
||||
}
|
||||
|
||||
if (window.UnifiedRunner.config.testingType === 'component') {
|
||||
if (window.__CYPRESS_TESTING_TYPE__ === 'component') {
|
||||
return runSpecCT(spec)
|
||||
}
|
||||
|
||||
throw Error('Unknown or undefined testingType on window.UnifiedRunner.config.testingType')
|
||||
throw Error('Unknown or undefined testingType on window.__CYPRESS_TESTING_TYPE__')
|
||||
}
|
||||
|
||||
function getAutomationElementId (): AutomationElementId {
|
||||
|
||||
@@ -73,6 +73,33 @@ describe('<ConfigCode />', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('sorts the config in alphabetical order', () => {
|
||||
let lastEntry = ''
|
||||
let nesting = 0
|
||||
let checkedFieldCount = 0
|
||||
const configFields = config.map((entry) => entry.field)
|
||||
|
||||
cy.get(selector).within(($selector) => {
|
||||
cy.get('span').each(($el: any) => {
|
||||
let configText = $el[0].innerText.split(':')[0]
|
||||
|
||||
if (configText === '{') {
|
||||
nesting++
|
||||
} else if (configText === '}') {
|
||||
nesting--
|
||||
}
|
||||
|
||||
if (nesting === 0 && configFields.includes(configText)) {
|
||||
expect(configText.localeCompare(lastEntry)).to.be.greaterThan(0)
|
||||
lastEntry = configText
|
||||
checkedFieldCount++
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
cy.then(() => expect(checkedFieldCount).to.eq(configFields.length))
|
||||
})
|
||||
|
||||
it('has an edit button', () => {
|
||||
cy.findByText(defaultMessages.file.edit).should('be.visible').click()
|
||||
})
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{<br>
|
||||
<div class="pl-24px">
|
||||
<span
|
||||
v-for="{ field, value, from } in props.gql.config"
|
||||
v-for="{ field, value, from } in sortAlphabetical(props.gql.config)"
|
||||
:key="field"
|
||||
>
|
||||
{{ field }}:
|
||||
@@ -74,6 +74,12 @@ const props = defineProps<{
|
||||
gql: ConfigCodeFragment
|
||||
}>()
|
||||
|
||||
const sortAlphabetical = (config) => {
|
||||
return config.sort((a, b) => {
|
||||
return a.field.localeCompare(b.field)
|
||||
})
|
||||
}
|
||||
|
||||
// a bug in vite demands that we do this passthrough
|
||||
const colorMap = CONFIG_LEGEND_COLOR_MAP
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
exports['config/lib/index .getBreakingKeys returns list of breaking config keys 1'] = [
|
||||
"componentFolder",
|
||||
"integrationFolder",
|
||||
"testFiles",
|
||||
"ignoreTestFiles",
|
||||
"pluginsFile",
|
||||
"experimentalComponentTesting",
|
||||
"blacklistHosts",
|
||||
"componentFolder",
|
||||
"experimentalComponentTesting",
|
||||
"experimentalGetCookiesSameSite",
|
||||
"experimentalNetworkStubbing",
|
||||
"experimentalRunEvents",
|
||||
"experimentalShadowDomSupport",
|
||||
"experimentalStudio",
|
||||
"firefoxGcInterval",
|
||||
"ignoreTestFiles",
|
||||
"integrationFolder",
|
||||
"nodeVersion",
|
||||
"nodeVersion"
|
||||
"nodeVersion",
|
||||
"pluginsFile",
|
||||
"testFiles"
|
||||
]
|
||||
|
||||
exports['config/lib/index .getDefaultValues returns list of public config keys 1'] = {
|
||||
|
||||
@@ -4,27 +4,37 @@
|
||||
"description": "Config contains the configuration types and validation function used in the cypress electron application.",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"browser": "src/index.ts",
|
||||
"browser": "src/browser.ts",
|
||||
"scripts": {
|
||||
"build-prod": "tsc || echo 'built, with errors'",
|
||||
"check-ts": "tsc --noEmit",
|
||||
"clean-deps": "rimraf node_modules",
|
||||
"clean": "rimraf --glob ./src/*.js ./src/**/*.js ./src/**/**/*.js ./test/**/*.js || echo 'cleaned'",
|
||||
"test": "yarn test-unit",
|
||||
"test:clean": "find ./test/__fixtures__ -depth -name 'output.*' -type f -exec rm {} \\;",
|
||||
"test-debug": "yarn test-unit --inspect-brk=5566",
|
||||
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register test/unit/**/*.spec.ts --exit"
|
||||
"test-unit": "mocha --configFile=../../mocha-reporter-config.json -r @packages/ts/register 'test/**/*.spec.ts' --exit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7",
|
||||
"@babel/parser": "^7",
|
||||
"@babel/plugin-syntax-typescript": "^7",
|
||||
"@babel/plugin-transform-typescript": "^7",
|
||||
"@babel/traverse": "^7",
|
||||
"@babel/types": "^7",
|
||||
"check-more-types": "2.24.0",
|
||||
"common-tags": "1.8.0",
|
||||
"debug": "^4.3.2",
|
||||
"lodash": "^4.17.21"
|
||||
"fs-extra": "^9.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"recast": "0.20.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@packages/root": "0.0.0-development",
|
||||
"@packages/ts": "0.0.0-development",
|
||||
"@packages/types": "0.0.0-development",
|
||||
"@types/mocha": "9.1.0",
|
||||
"babel-plugin-tester": "^10.1.0",
|
||||
"chai": "4.2.0",
|
||||
"mocha": "7.0.1",
|
||||
"rimraf": "3.0.2"
|
||||
|
||||
170
packages/config/src/ast-utils/addToCypressConfig.ts
Normal file
170
packages/config/src/ast-utils/addToCypressConfig.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import * as t from '@babel/types'
|
||||
import traverse from '@babel/traverse'
|
||||
import fs from 'fs-extra'
|
||||
import dedent from 'dedent'
|
||||
import path from 'path'
|
||||
import debugLib from 'debug'
|
||||
import { parse, print } from 'recast'
|
||||
|
||||
import { addToCypressConfigPlugin } from './addToCypressConfigPlugin'
|
||||
import { addComponentDefinition, addE2EDefinition, ASTComponentDefinitionConfig } from './astConfigHelpers'
|
||||
|
||||
const debug = debugLib('cypress:config:addToCypressConfig')
|
||||
|
||||
/**
|
||||
* Adds to the Cypress config, using the Babel AST utils.
|
||||
*
|
||||
* Injects the export at the top of the config definition, based on the common patterns of:
|
||||
*
|
||||
* export default { ...
|
||||
*
|
||||
* export default defineConfig({ ...
|
||||
*
|
||||
* module.exports = { ...
|
||||
*
|
||||
* module.exports = defineConfig({ ...
|
||||
*
|
||||
* export = { ...
|
||||
*
|
||||
* export = defineConfig({ ...
|
||||
*
|
||||
* If we don't match one of these, we'll use the rest-spread pattern on whatever
|
||||
* the current default export of the file is:
|
||||
*
|
||||
* current:
|
||||
* export default createConfigFn()
|
||||
*
|
||||
* becomes:
|
||||
* export default {
|
||||
* projectId: '...',
|
||||
* ...createConfigFn()
|
||||
* }
|
||||
*/
|
||||
export async function addToCypressConfig (filePath: string, code: string, toAdd: t.ObjectProperty) {
|
||||
try {
|
||||
const ast = parse(code, {
|
||||
parser: require('recast/parsers/typescript'),
|
||||
})
|
||||
|
||||
traverse(ast, addToCypressConfigPlugin(toAdd).visitor)
|
||||
|
||||
return print(ast).code
|
||||
} catch (e) {
|
||||
debug(`Error adding properties to %s: %s`, filePath, e.stack)
|
||||
throw new Error(`Unable to automerge with the config file`)
|
||||
}
|
||||
}
|
||||
|
||||
export interface AddProjectIdToCypressConfigOptions {
|
||||
filePath: string
|
||||
projectId: string
|
||||
}
|
||||
|
||||
export async function addProjectIdToCypressConfig (options: AddProjectIdToCypressConfigOptions) {
|
||||
try {
|
||||
let result = await fs.readFile(options.filePath, 'utf8')
|
||||
const toPrint = await addToCypressConfig(options.filePath, result, t.objectProperty(
|
||||
t.identifier('projectId'),
|
||||
t.identifier(options.projectId),
|
||||
))
|
||||
|
||||
await fs.writeFile(options.filePath, maybeFormatWithPrettier(toPrint, options.filePath))
|
||||
|
||||
return {
|
||||
result: 'MERGED',
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
result: 'NEEDS_MERGE',
|
||||
error: e,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AddToCypressConfigResult {
|
||||
result: 'ADDED' | 'MERGED' | 'NEEDS_MERGE'
|
||||
error?: Error
|
||||
codeToMerge?: string
|
||||
}
|
||||
|
||||
export interface AddTestingTypeToCypressConfigOptions {
|
||||
filePath: string
|
||||
info: ASTComponentDefinitionConfig | {
|
||||
testingType: 'e2e'
|
||||
}
|
||||
}
|
||||
|
||||
export async function addTestingTypeToCypressConfig (options: AddTestingTypeToCypressConfigOptions): Promise<AddToCypressConfigResult> {
|
||||
const toAdd = options.info.testingType === 'e2e' ? addE2EDefinition() : addComponentDefinition(options.info)
|
||||
|
||||
try {
|
||||
let result: string | undefined = undefined
|
||||
let resultStatus: 'ADDED' | 'MERGED' = 'MERGED'
|
||||
|
||||
try {
|
||||
result = await fs.readFile(options.filePath, 'utf8')
|
||||
} catch (e) {
|
||||
// If we can't find the file, or it's an empty file, let's create a new one
|
||||
}
|
||||
|
||||
const pathExt = path.extname(options.filePath)
|
||||
|
||||
// If for some reason they have deleted the contents of the file, we want to recover
|
||||
// gracefully by adding some default code to use as the AST here, based on the extension
|
||||
if (!result || result.trim() === '') {
|
||||
resultStatus = 'ADDED'
|
||||
result = getEmptyCodeBlock(pathExt as OutputExtension)
|
||||
}
|
||||
|
||||
const toPrint = await addToCypressConfig(options.filePath, result, toAdd)
|
||||
|
||||
await fs.writeFile(options.filePath, maybeFormatWithPrettier(toPrint, options.filePath))
|
||||
|
||||
return {
|
||||
result: resultStatus,
|
||||
}
|
||||
} catch (e) {
|
||||
return {
|
||||
result: 'NEEDS_MERGE',
|
||||
error: e,
|
||||
codeToMerge: print(toAdd).code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type OutputExtension = '.ts' | '.mjs' | '.js'
|
||||
|
||||
// Necessary to handle the edge case of them deleting the contents of their Cypress
|
||||
// config file, just before we merge in the testing type
|
||||
function getEmptyCodeBlock (outputType: OutputExtension) {
|
||||
if (outputType === '.ts' || outputType === '.mjs') {
|
||||
return dedent`
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
return dedent`
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
function maybeFormatWithPrettier (code: string, filePath: string) {
|
||||
try {
|
||||
const prettier = require('prettier') as typeof import('prettier')
|
||||
|
||||
return prettier.format(code, {
|
||||
filepath: filePath,
|
||||
})
|
||||
} catch {
|
||||
//
|
||||
return code
|
||||
}
|
||||
}
|
||||
237
packages/config/src/ast-utils/addToCypressConfigPlugin.ts
Normal file
237
packages/config/src/ast-utils/addToCypressConfigPlugin.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type { NodePath, ParserOptions, PluginObj, Visitor } from '@babel/core'
|
||||
import * as t from '@babel/types'
|
||||
import debugLib from 'debug'
|
||||
import { print } from 'recast'
|
||||
|
||||
const debug = debugLib('cypress:config:addToCypressConfigPlugin')
|
||||
|
||||
interface AddToCypressConfigPluginOptions {
|
||||
shouldThrow?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardizes our approach to writing values into the existing
|
||||
* Cypress config file. Attempts to handle the pragmatic cases,
|
||||
* finding the typical patterns we'd expect to see for `defineConfig`
|
||||
* import & usage, falling back to adding spread object properties
|
||||
* on `module.exports` or `export default`
|
||||
*
|
||||
* @param toAdd k/v Object Property to append to the current object
|
||||
* @returns
|
||||
*/
|
||||
export function addToCypressConfigPlugin (toAdd: t.ObjectProperty, opts: AddToCypressConfigPluginOptions = {}): PluginObj<any> {
|
||||
debug(`adding %s`, toAdd)
|
||||
const { shouldThrow = true } = opts
|
||||
|
||||
function canAddKey (path: NodePath<any>, props: t.ObjectExpression['properties'], toAdd: t.ObjectProperty) {
|
||||
for (const prop of props) {
|
||||
if (t.isObjectProperty(prop) && t.isNodesEquivalent(prop['key'], toAdd['key'])) {
|
||||
if (shouldThrow) {
|
||||
throw new Error(`Cannot add, the existing config has a ${print(prop['key']).code} property`)
|
||||
} else {
|
||||
path.stop()
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on the import syntax, we look for the "defineConfig" identifier, and whether it
|
||||
* has been reassigned
|
||||
*/
|
||||
const defineConfigIdentifiers: Array<string | [string, string]> = []
|
||||
/**
|
||||
* Checks whether we've seen the identifier
|
||||
*/
|
||||
let seenConfigIdentifierCall = false
|
||||
|
||||
// Returns the ObjectExpression associated with the defineConfig call,
|
||||
// so we can add in the "toAdd" object property
|
||||
function getDefineConfigExpression (node: t.CallExpression): t.ObjectExpression | undefined {
|
||||
for (const possibleIdentifier of defineConfigIdentifiers) {
|
||||
if (typeof possibleIdentifier === 'string') {
|
||||
if (t.isIdentifier(node.callee) && node.callee.name === possibleIdentifier && t.isObjectExpression(node.arguments[0])) {
|
||||
return node.arguments[0]
|
||||
}
|
||||
} else if (Array.isArray(possibleIdentifier)) {
|
||||
if (t.isMemberExpression(node.callee) &&
|
||||
t.isIdentifier(node.callee.object) &&
|
||||
t.isIdentifier(node.callee.property) &&
|
||||
node.callee.object.name === possibleIdentifier[0] &&
|
||||
node.callee.property.name === possibleIdentifier[1] &&
|
||||
t.isObjectExpression(node.arguments[0])
|
||||
) {
|
||||
return node.arguments[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Visits the program ahead-of-time, to know what transforms we need to do
|
||||
// on the source when we output the addition
|
||||
const nestedVisitor: Visitor = {
|
||||
ImportDeclaration (path) {
|
||||
// Skip "import type" for the purpose of finding the defineConfig identifier,
|
||||
// and skip if we see a non "cypress" import, since that's the only one we care about finding
|
||||
if (path.node.importKind === 'type' || path.node.source.value !== 'cypress') {
|
||||
debug(`Skipping non-cypress import declaration %s`, path)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const specifier of path.node.specifiers) {
|
||||
if (specifier.type === 'ImportNamespaceSpecifier' || specifier.type === 'ImportDefaultSpecifier') {
|
||||
debug(`Adding %s specifier [%s, %s]`, specifier.type, specifier.local.name, 'defineConfig')
|
||||
defineConfigIdentifiers.push([specifier.local.name, 'defineConfig'])
|
||||
} else {
|
||||
debug(`Adding import specifier %s`, specifier.local.name)
|
||||
defineConfigIdentifiers.push(specifier.local.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
VariableDeclaration (path) {
|
||||
// We only care about the top-level variable declarations for requires
|
||||
if (path.parent.type !== 'Program') {
|
||||
return
|
||||
}
|
||||
|
||||
const cyRequireDeclaration = path.node.declarations.filter((d) => {
|
||||
return (
|
||||
t.isCallExpression(d.init) &&
|
||||
t.isIdentifier(d.init.callee) &&
|
||||
d.init.callee.name === 'require' &&
|
||||
t.isStringLiteral(d.init.arguments[0]) &&
|
||||
d.init.arguments[0].value === 'cypress'
|
||||
)
|
||||
})
|
||||
|
||||
for (const variableDeclaration of cyRequireDeclaration) {
|
||||
if (t.isIdentifier(variableDeclaration.id)) {
|
||||
debug(`Cypress require declaration [%s, 'defineConfig']`, variableDeclaration.id.name)
|
||||
defineConfigIdentifiers.push([variableDeclaration.id.name, 'defineConfig'])
|
||||
} else if (t.isObjectPattern(variableDeclaration.id)) {
|
||||
for (const prop of variableDeclaration.id.properties) {
|
||||
if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && t.isIdentifier(prop.value)) {
|
||||
if (prop.key.name === 'defineConfig') {
|
||||
debug(`Adding destructured object prop`, prop.value.name)
|
||||
defineConfigIdentifiers.push(prop.value.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug(`Skipping variableDeclaration %s`, variableDeclaration.id.type)
|
||||
}
|
||||
}
|
||||
},
|
||||
CallExpression (path) {
|
||||
if (getDefineConfigExpression(path.node)) {
|
||||
seenConfigIdentifierCall = true
|
||||
debug(`Seen identifier call %s`, path)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
let didAdd = false
|
||||
|
||||
return {
|
||||
name: 'addToCypressConfigPlugin',
|
||||
manipulateOptions (t, parserOpts: ParserOptions) {
|
||||
parserOpts.errorRecovery = true
|
||||
parserOpts.plugins ??= []
|
||||
if (
|
||||
parserOpts.plugins.some(
|
||||
(p: any) => (Array.isArray(p) ? p[0] : p) === 'typescript',
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
parserOpts.plugins.push('typescript')
|
||||
},
|
||||
visitor: {
|
||||
Program: {
|
||||
enter (path) {
|
||||
path.traverse(nestedVisitor)
|
||||
debug(`Finished initial traversal, seenConfigIdentifierCall: %s - %o`, seenConfigIdentifierCall, defineConfigIdentifiers)
|
||||
},
|
||||
exit () {
|
||||
if (!didAdd && shouldThrow) {
|
||||
throw new Error(`Unable to add properties`)
|
||||
}
|
||||
},
|
||||
},
|
||||
CallExpression (path) {
|
||||
if (seenConfigIdentifierCall && !didAdd) {
|
||||
const defineConfigExpression = getDefineConfigExpression(path.node)
|
||||
|
||||
if (defineConfigExpression) {
|
||||
if (canAddKey(path, defineConfigExpression.properties, toAdd)) {
|
||||
defineConfigExpression.properties.push(toAdd)
|
||||
didAdd = true
|
||||
debug(`Added to defineConfig expression`)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
ExportDefaultDeclaration (path) {
|
||||
// Exit if we've seen the defineConfig({ ... called elsewhere,
|
||||
// since this is where we'll be adding the object props
|
||||
if (seenConfigIdentifierCall || didAdd) {
|
||||
return
|
||||
}
|
||||
|
||||
// export default {}
|
||||
if (t.isObjectExpression(path.node.declaration)) {
|
||||
if (canAddKey(path, path.node.declaration.properties, toAdd)) {
|
||||
path.node.declaration.properties.push(toAdd)
|
||||
didAdd = true
|
||||
}
|
||||
} else if (t.isExpression(path.node.declaration)) {
|
||||
path.node.declaration = spreadResult(path.node.declaration, toAdd)
|
||||
didAdd = true
|
||||
}
|
||||
},
|
||||
AssignmentExpression (path) {
|
||||
// Exit if we've seen the defineConfig({ ... called elsewhere,
|
||||
// since this is where we'll be adding the object props
|
||||
if (seenConfigIdentifierCall || didAdd) {
|
||||
return
|
||||
}
|
||||
|
||||
if (t.isMemberExpression(path.node.left) && isModuleExports(path.node.left)) {
|
||||
if (t.isObjectExpression(path.node.right)) {
|
||||
if (canAddKey(path, path.node.right.properties, toAdd)) {
|
||||
path.node.right.properties.push(toAdd)
|
||||
didAdd = true
|
||||
}
|
||||
} else if (t.isExpression(path.node.right)) {
|
||||
path.node.right = spreadResult(path.node.right, toAdd)
|
||||
didAdd = true
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function spreadResult (expr: t.Expression, toAdd: t.ObjectProperty): t.ObjectExpression {
|
||||
return t.objectExpression([
|
||||
t.spreadElement(expr),
|
||||
toAdd,
|
||||
])
|
||||
}
|
||||
|
||||
function isModuleExports (node: t.MemberExpression) {
|
||||
return (
|
||||
t.isIdentifier(node.object) &&
|
||||
node.object.name === 'module' &&
|
||||
t.isIdentifier(node.property) &&
|
||||
node.property.name === 'exports'
|
||||
)
|
||||
}
|
||||
82
packages/config/src/ast-utils/astConfigHelpers.ts
Normal file
82
packages/config/src/ast-utils/astConfigHelpers.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import * as t from '@babel/types'
|
||||
import { parse, visit } from 'recast'
|
||||
import dedent from 'dedent'
|
||||
import assert from 'assert'
|
||||
|
||||
/**
|
||||
* AST definition Node for:
|
||||
*
|
||||
* e2e: {
|
||||
* setupNodeEvents(on, config) {
|
||||
* // implement node event listeners here
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function addE2EDefinition (): t.ObjectProperty {
|
||||
return extractProperty(`
|
||||
const toMerge = {
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
}
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
export interface ASTComponentDefinitionConfig {
|
||||
testingType: 'component'
|
||||
bundler: 'vite' | 'webpack'
|
||||
framework?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* AST definition Node for:
|
||||
*
|
||||
* component: {
|
||||
* devServer: {
|
||||
* bundler: 'bundler',
|
||||
* framework: 'framework',
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export function addComponentDefinition (config: ASTComponentDefinitionConfig): t.ObjectProperty {
|
||||
return extractProperty(`
|
||||
const toMerge = {
|
||||
component: {
|
||||
devServer: {
|
||||
framework: ${config.framework ? `'${config.framework}'` : 'undefined'},
|
||||
bundler: '${config.bundler}',
|
||||
},
|
||||
},
|
||||
}
|
||||
`)
|
||||
}
|
||||
|
||||
function extractProperty (str: string) {
|
||||
const toParse = parse(dedent(str), {
|
||||
parser: require('recast/parsers/typescript'),
|
||||
})
|
||||
|
||||
let complete = false
|
||||
let toAdd: t.ObjectProperty | undefined
|
||||
|
||||
visit(toParse, {
|
||||
visitObjectExpression (path) {
|
||||
if (complete) return false
|
||||
|
||||
if (path.node.properties.length > 1 || !t.isObjectProperty(path.node.properties[0])) {
|
||||
throw new Error(`Can only parse an expression with a single property`)
|
||||
}
|
||||
|
||||
toAdd = path.node.properties[0]
|
||||
complete = true
|
||||
|
||||
return false
|
||||
},
|
||||
})
|
||||
|
||||
assert(toAdd, `Missing property to merge into config from string: ${str}`)
|
||||
|
||||
return toAdd
|
||||
}
|
||||
180
packages/config/src/browser.ts
Normal file
180
packages/config/src/browser.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import _ from 'lodash'
|
||||
import Debug from 'debug'
|
||||
import { defaultSpecPattern, options, breakingOptions, breakingRootOptions, testingTypeBreakingOptions, additionalOptionsToResolveConfig } from './options'
|
||||
import type { BreakingOption, BreakingOptionErrorKey } from './options'
|
||||
import type { TestingType } from '@packages/types'
|
||||
|
||||
// this export has to be done in 2 lines because of a bug in babel typescript
|
||||
import * as validation from './validation'
|
||||
|
||||
export {
|
||||
defaultSpecPattern,
|
||||
validation,
|
||||
options,
|
||||
breakingOptions,
|
||||
BreakingOption,
|
||||
BreakingOptionErrorKey,
|
||||
}
|
||||
|
||||
const debug = Debug('cypress:config:validator')
|
||||
|
||||
const dashesOrUnderscoresRe = /^(_-)+/
|
||||
|
||||
// takes an array and creates an index object of [keyKey]: [valueKey]
|
||||
function createIndex<T extends Record<string, any>> (arr: Array<T>, keyKey: keyof T, valueKey: keyof T) {
|
||||
return _.reduce(arr, (memo: Record<string, any>, item) => {
|
||||
if (item[valueKey] !== undefined) {
|
||||
memo[item[keyKey] as string] = item[valueKey]
|
||||
}
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
const breakingKeys = _.map(breakingOptions, 'name')
|
||||
const defaultValues = createIndex(options, 'name', 'defaultValue')
|
||||
const publicConfigKeys = _([...options, ...additionalOptionsToResolveConfig]).reject({ isInternal: true }).map('name').value()
|
||||
const validationRules = createIndex(options, 'name', 'validation')
|
||||
const testConfigOverrideOptions = createIndex(options, 'name', 'canUpdateDuringTestTime')
|
||||
|
||||
const issuedWarnings = new Set()
|
||||
|
||||
export type BreakingErrResult = {
|
||||
name: string
|
||||
newName?: string
|
||||
value?: any
|
||||
configFile: string
|
||||
testingType?: TestingType
|
||||
}
|
||||
|
||||
type ErrorHandler = (
|
||||
key: BreakingOptionErrorKey,
|
||||
options: BreakingErrResult
|
||||
) => void
|
||||
|
||||
const validateNoBreakingOptions = (breakingCfgOptions: BreakingOption[], cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType?: TestingType) => {
|
||||
breakingCfgOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
|
||||
if (_.has(cfg, name)) {
|
||||
if (value && cfg[name] !== value) {
|
||||
// Bail if a value is specified but the config does not have that value.
|
||||
return
|
||||
}
|
||||
|
||||
if (isWarning) {
|
||||
if (issuedWarnings.has(errorKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// avoid re-issuing the same warning more than once
|
||||
issuedWarnings.add(errorKey)
|
||||
|
||||
return onWarning(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
testingType,
|
||||
})
|
||||
}
|
||||
|
||||
return onErr(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
testingType,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allowed = (obj = {}) => {
|
||||
const propertyNames = publicConfigKeys.concat(breakingKeys)
|
||||
|
||||
return _.pick(obj, propertyNames)
|
||||
}
|
||||
|
||||
export const getBreakingKeys = () => {
|
||||
return breakingKeys
|
||||
}
|
||||
|
||||
export const getBreakingRootKeys = () => {
|
||||
return breakingRootOptions
|
||||
}
|
||||
|
||||
export const getDefaultValues = (runtimeOptions: { [k: string]: any } = {}) => {
|
||||
// Default values can be functions, in which case they are evaluated
|
||||
// at runtime - for example, slowTestThreshold where the default value
|
||||
// varies between e2e and component testing.
|
||||
const defaultsForRuntime = _.mapValues(defaultValues, (value) => (typeof value === 'function' ? value(runtimeOptions) : value))
|
||||
|
||||
// As we normalize the config based on the selected testing type, we need
|
||||
// to do the same with the default values to resolve those correctly
|
||||
return { ...defaultsForRuntime, ...defaultsForRuntime[runtimeOptions.testingType] }
|
||||
}
|
||||
|
||||
export const getPublicConfigKeys = () => {
|
||||
return publicConfigKeys
|
||||
}
|
||||
|
||||
export const matchesConfigKey = (key: string) => {
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
|
||||
key = _.camelCase(key)
|
||||
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export const validate = (cfg: any, onErr: (property: string) => void) => {
|
||||
debug('validating configuration', cfg)
|
||||
|
||||
return _.each(cfg, (value, key) => {
|
||||
const validationFn = validationRules[key]
|
||||
|
||||
// key has a validation rule & value different from the default
|
||||
if (validationFn && value !== defaultValues[key]) {
|
||||
const result = validationFn(key, value)
|
||||
|
||||
if (result !== true) {
|
||||
return onErr(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigRoot = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType: TestingType) => {
|
||||
return validateNoBreakingOptions(breakingRootOptions, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfig = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType: TestingType) => {
|
||||
return validateNoBreakingOptions(breakingOptions, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigLaunchpad = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
return validateNoBreakingOptions(breakingOptions.filter((option) => option.showInLaunchpad), cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoBreakingTestingTypeConfig = (cfg: any, testingType: keyof typeof testingTypeBreakingOptions, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
const options = testingTypeBreakingOptions[testingType]
|
||||
|
||||
return validateNoBreakingOptions(options, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoReadOnlyConfig = (config: any, onErr: (property: string) => void) => {
|
||||
let errProperty
|
||||
|
||||
Object.keys(config).some((option) => {
|
||||
return errProperty = testConfigOverrideOptions[option] === false ? option : undefined
|
||||
})
|
||||
|
||||
if (errProperty) {
|
||||
return onErr(errProperty)
|
||||
}
|
||||
}
|
||||
@@ -1,180 +1,5 @@
|
||||
import _ from 'lodash'
|
||||
import Debug from 'debug'
|
||||
import { defaultSpecPattern, options, breakingOptions, breakingRootOptions, testingTypeBreakingOptions, additionalOptionsToResolveConfig } from './options'
|
||||
import type { BreakingOption, BreakingOptionErrorKey } from './options'
|
||||
import type { TestingType } from '@packages/types'
|
||||
// Separating this, so we don't pull all of the server side
|
||||
// babel transforms, etc. into client-side usage of the config code
|
||||
export * from './browser'
|
||||
|
||||
// this export has to be done in 2 lines because of a bug in babel typescript
|
||||
import * as validation from './validation'
|
||||
|
||||
export {
|
||||
defaultSpecPattern,
|
||||
validation,
|
||||
options,
|
||||
breakingOptions,
|
||||
BreakingOption,
|
||||
BreakingOptionErrorKey,
|
||||
}
|
||||
|
||||
const debug = Debug('cypress:config:validator')
|
||||
|
||||
const dashesOrUnderscoresRe = /^(_-)+/
|
||||
|
||||
// takes an array and creates an index object of [keyKey]: [valueKey]
|
||||
function createIndex<T extends Record<string, any>> (arr: Array<T>, keyKey: keyof T, valueKey: keyof T) {
|
||||
return _.reduce(arr, (memo: Record<string, any>, item) => {
|
||||
if (item[valueKey] !== undefined) {
|
||||
memo[item[keyKey] as string] = item[valueKey]
|
||||
}
|
||||
|
||||
return memo
|
||||
}, {})
|
||||
}
|
||||
|
||||
const breakingKeys = _.map(breakingOptions, 'name')
|
||||
const defaultValues = createIndex(options, 'name', 'defaultValue')
|
||||
const publicConfigKeys = _([...options, ...additionalOptionsToResolveConfig]).reject({ isInternal: true }).map('name').value()
|
||||
const validationRules = createIndex(options, 'name', 'validation')
|
||||
const testConfigOverrideOptions = createIndex(options, 'name', 'canUpdateDuringTestTime')
|
||||
|
||||
const issuedWarnings = new Set()
|
||||
|
||||
export type BreakingErrResult = {
|
||||
name: string
|
||||
newName?: string
|
||||
value?: any
|
||||
configFile: string
|
||||
testingType?: TestingType
|
||||
}
|
||||
|
||||
type ErrorHandler = (
|
||||
key: BreakingOptionErrorKey,
|
||||
options: BreakingErrResult
|
||||
) => void
|
||||
|
||||
const validateNoBreakingOptions = (breakingCfgOptions: BreakingOption[], cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType?: TestingType) => {
|
||||
breakingCfgOptions.forEach(({ name, errorKey, newName, isWarning, value }) => {
|
||||
if (_.has(cfg, name)) {
|
||||
if (value && cfg[name] !== value) {
|
||||
// Bail if a value is specified but the config does not have that value.
|
||||
return
|
||||
}
|
||||
|
||||
if (isWarning) {
|
||||
if (issuedWarnings.has(errorKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// avoid re-issuing the same warning more than once
|
||||
issuedWarnings.add(errorKey)
|
||||
|
||||
return onWarning(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
testingType,
|
||||
})
|
||||
}
|
||||
|
||||
return onErr(errorKey, {
|
||||
name,
|
||||
newName,
|
||||
value,
|
||||
configFile: cfg.configFile,
|
||||
testingType,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const allowed = (obj = {}) => {
|
||||
const propertyNames = publicConfigKeys.concat(breakingKeys)
|
||||
|
||||
return _.pick(obj, propertyNames)
|
||||
}
|
||||
|
||||
export const getBreakingKeys = () => {
|
||||
return breakingKeys
|
||||
}
|
||||
|
||||
export const getBreakingRootKeys = () => {
|
||||
return breakingRootOptions
|
||||
}
|
||||
|
||||
export const getDefaultValues = (runtimeOptions: { [k: string]: any } = {}) => {
|
||||
// Default values can be functions, in which case they are evaluated
|
||||
// at runtime - for example, slowTestThreshold where the default value
|
||||
// varies between e2e and component testing.
|
||||
const defaultsForRuntime = _.mapValues(defaultValues, (value) => (typeof value === 'function' ? value(runtimeOptions) : value))
|
||||
|
||||
// As we normalize the config based on the selected testing type, we need
|
||||
// to do the same with the default values to resolve those correctly
|
||||
return { ...defaultsForRuntime, ...defaultsForRuntime[runtimeOptions.testingType] }
|
||||
}
|
||||
|
||||
export const getPublicConfigKeys = () => {
|
||||
return publicConfigKeys
|
||||
}
|
||||
|
||||
export const matchesConfigKey = (key: string) => {
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
key = key.toLowerCase().replace(dashesOrUnderscoresRe, '')
|
||||
key = _.camelCase(key)
|
||||
|
||||
if (_.has(defaultValues, key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export const validate = (cfg: any, onErr: (property: string) => void) => {
|
||||
debug('validating configuration', cfg)
|
||||
|
||||
return _.each(cfg, (value, key) => {
|
||||
const validationFn = validationRules[key]
|
||||
|
||||
// key has a validation rule & value different from the default
|
||||
if (validationFn && value !== defaultValues[key]) {
|
||||
const result = validationFn(key, value)
|
||||
|
||||
if (result !== true) {
|
||||
return onErr(result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigRoot = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType: TestingType) => {
|
||||
return validateNoBreakingOptions(breakingRootOptions, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfig = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType: TestingType) => {
|
||||
return validateNoBreakingOptions(breakingOptions, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoBreakingConfigLaunchpad = (cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
return validateNoBreakingOptions(breakingOptions.filter((option) => option.showInLaunchpad), cfg, onWarning, onErr)
|
||||
}
|
||||
|
||||
export const validateNoBreakingTestingTypeConfig = (cfg: any, testingType: keyof typeof testingTypeBreakingOptions, onWarning: ErrorHandler, onErr: ErrorHandler) => {
|
||||
const options = testingTypeBreakingOptions[testingType]
|
||||
|
||||
return validateNoBreakingOptions(options, cfg, onWarning, onErr, testingType)
|
||||
}
|
||||
|
||||
export const validateNoReadOnlyConfig = (config: any, onErr: (property: string) => void) => {
|
||||
let errProperty
|
||||
|
||||
Object.keys(config).some((option) => {
|
||||
return errProperty = testConfigOverrideOptions[option] === false ? option : undefined
|
||||
})
|
||||
|
||||
if (errProperty) {
|
||||
return onErr(errProperty)
|
||||
}
|
||||
}
|
||||
export { addProjectIdToCypressConfig, addToCypressConfig, addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from './ast-utils/addToCypressConfig'
|
||||
|
||||
@@ -407,6 +407,16 @@ const resolvedOptions: Array<ResolvedConfigOption> = [
|
||||
|
||||
const runtimeOptions: Array<RuntimeConfigOption> = [
|
||||
{
|
||||
// Internal config field, useful to ignore the e2e specPattern set by the user
|
||||
// or the default one when looking fot CT, it needs to be a config property because after
|
||||
// having the final config that has the e2e property flattened/compacted
|
||||
// we may not be able to get the value to ignore.
|
||||
name: 'additionalIgnorePattern',
|
||||
defaultValue: (options: Record<string, any> = {}) => options.testingType === 'component' ? defaultSpecPattern.e2e : undefined,
|
||||
validation: validate.isString,
|
||||
isInternal: true,
|
||||
canUpdateDuringTestTime: false,
|
||||
}, {
|
||||
name: 'autoOpen',
|
||||
defaultValue: false,
|
||||
validation: validate.isBoolean,
|
||||
@@ -511,16 +521,6 @@ const runtimeOptions: Array<RuntimeConfigOption> = [
|
||||
validation: validate.isString,
|
||||
isInternal: true,
|
||||
canUpdateDuringTestTime: false,
|
||||
}, {
|
||||
// Internal config field, useful to ignore the e2e specPattern set by the user
|
||||
// or the default one when looking fot CT, it needs to be a config property because after
|
||||
// having the final config that has the e2e property flattened/compacted
|
||||
// we may not be able to get the value to ignore.
|
||||
name: 'additionalIgnorePattern',
|
||||
defaultValue: (options: Record<string, any> = {}) => options.testingType === 'component' ? defaultSpecPattern.e2e : undefined,
|
||||
validation: validate.isString,
|
||||
isInternal: true,
|
||||
canUpdateDuringTestTime: false,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -543,36 +543,18 @@ export const additionalOptionsToResolveConfig = [
|
||||
*/
|
||||
export const breakingOptions: Array<BreakingOption> = [
|
||||
{
|
||||
name: 'blacklistHosts',
|
||||
errorKey: 'RENAMED_CONFIG_OPTION',
|
||||
newName: 'blockHosts',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'componentFolder',
|
||||
errorKey: 'COMPONENT_FOLDER_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'integrationFolder',
|
||||
errorKey: 'INTEGRATION_FOLDER_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'testFiles',
|
||||
errorKey: 'TEST_FILES_RENAMED',
|
||||
newName: 'specPattern',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'ignoreTestFiles',
|
||||
errorKey: 'TEST_FILES_RENAMED',
|
||||
newName: 'excludeSpecPattern',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'pluginsFile',
|
||||
errorKey: 'PLUGINS_FILE_CONFIG_OPTION_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'experimentalComponentTesting',
|
||||
errorKey: 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'blacklistHosts',
|
||||
errorKey: 'RENAMED_CONFIG_OPTION',
|
||||
newName: 'blockHosts',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'experimentalGetCookiesSameSite',
|
||||
errorKey: 'EXPERIMENTAL_SAMESITE_REMOVED',
|
||||
@@ -598,6 +580,15 @@ export const breakingOptions: Array<BreakingOption> = [
|
||||
name: 'firefoxGcInterval',
|
||||
errorKey: 'FIREFOX_GC_INTERVAL_REMOVED',
|
||||
isWarning: true,
|
||||
}, {
|
||||
name: 'ignoreTestFiles',
|
||||
errorKey: 'TEST_FILES_RENAMED',
|
||||
newName: 'excludeSpecPattern',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'integrationFolder',
|
||||
errorKey: 'INTEGRATION_FOLDER_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'nodeVersion',
|
||||
value: 'system',
|
||||
@@ -608,51 +599,54 @@ export const breakingOptions: Array<BreakingOption> = [
|
||||
value: 'bundled',
|
||||
errorKey: 'NODE_VERSION_DEPRECATION_BUNDLED',
|
||||
isWarning: true,
|
||||
}, {
|
||||
name: 'pluginsFile',
|
||||
errorKey: 'PLUGINS_FILE_CONFIG_OPTION_REMOVED',
|
||||
isWarning: false,
|
||||
}, {
|
||||
name: 'testFiles',
|
||||
errorKey: 'TEST_FILES_RENAMED',
|
||||
newName: 'specPattern',
|
||||
isWarning: false,
|
||||
},
|
||||
]
|
||||
|
||||
export const breakingRootOptions: Array<BreakingOption> = [
|
||||
{
|
||||
name: 'supportFile',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'specPattern',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'excludeSpecPattern',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'experimentalStudio',
|
||||
errorKey: 'EXPERIMENTAL_STUDIO_REMOVED',
|
||||
isWarning: true,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
name: 'baseUrl',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E',
|
||||
isWarning: false,
|
||||
testingTypes: ['e2e'],
|
||||
},
|
||||
{
|
||||
name: 'slowTestThreshold',
|
||||
}, {
|
||||
name: 'excludeSpecPattern',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
{
|
||||
}, {
|
||||
name: 'experimentalStudio',
|
||||
errorKey: 'EXPERIMENTAL_STUDIO_REMOVED',
|
||||
isWarning: true,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
}, {
|
||||
name: 'indexHtmlFile',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT',
|
||||
isWarning: false,
|
||||
testingTypes: ['component'],
|
||||
}, {
|
||||
name: 'slowTestThreshold',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
}, {
|
||||
name: 'specPattern',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
}, {
|
||||
name: 'supportFile',
|
||||
errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG',
|
||||
isWarning: false,
|
||||
testingTypes: ['component', 'e2e'],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
const myConfig = defineConfig({
|
||||
e2e: {}
|
||||
})
|
||||
|
||||
export default myConfig
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from "cypress";
|
||||
const myConfig = defineConfig({
|
||||
e2e: {},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
});
|
||||
export default myConfig;
|
||||
@@ -0,0 +1,4 @@
|
||||
const { defineConfig: cypressDefineConfig } = require('cypress')
|
||||
|
||||
export default cypressDefineConfig({
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
const { defineConfig: cypressDefineConfig } = require("cypress");
|
||||
|
||||
export default cypressDefineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
e2e: {},
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default {
|
||||
e2e: {},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineConfig as myDefineConfig } from 'cypress'
|
||||
|
||||
export default myDefineConfig({
|
||||
e2e: {},
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig as myDefineConfig } from "cypress";
|
||||
export default myDefineConfig({
|
||||
e2e: {},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import cy from 'cypress'
|
||||
|
||||
export default cy.defineConfig({
|
||||
e2e: {},
|
||||
})
|
||||
@@ -0,0 +1,10 @@
|
||||
import cy from "cypress";
|
||||
export default cy.defineConfig({
|
||||
e2e: {},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
e2e: {},
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {},
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineConfig } from "cypress";
|
||||
export default defineConfig({
|
||||
e2e: {},
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
module.exports = {
|
||||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
component: {
|
||||
supportFile: false,
|
||||
devServer: {
|
||||
framework: 'react',
|
||||
bundler: 'webpack',
|
||||
webpackConfig: require('./webpack.config'),
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from "cypress";
|
||||
export default defineConfig({
|
||||
component: {
|
||||
devServer: {
|
||||
framework: "react",
|
||||
bundler: "webpack",
|
||||
},
|
||||
},
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
3
packages/config/test/__fixtures__/has-e2e.config.ts
Normal file
3
packages/config/test/__fixtures__/has-e2e.config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
e2e: {}
|
||||
}
|
||||
1
packages/config/test/__fixtures__/invalid.config.ts
Normal file
1
packages/config/test/__fixtures__/invalid.config.ts
Normal file
@@ -0,0 +1 @@
|
||||
const x = {}
|
||||
31
packages/config/test/ast-utils/addPluginId.spec.ts
Normal file
31
packages/config/test/ast-utils/addPluginId.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import pluginTester from 'babel-plugin-tester'
|
||||
import * as t from '@babel/types'
|
||||
import dedent from 'dedent'
|
||||
|
||||
import { addToCypressConfigPlugin } from '../../src/ast-utils/addToCypressConfigPlugin'
|
||||
|
||||
pluginTester({
|
||||
pluginName: 'addPluginId',
|
||||
plugin: addToCypressConfigPlugin(
|
||||
t.objectProperty(
|
||||
t.identifier('projectId'),
|
||||
t.stringLiteral('abc1234'),
|
||||
),
|
||||
{ shouldThrow: false },
|
||||
),
|
||||
tests: [
|
||||
{
|
||||
code: dedent`
|
||||
export default {
|
||||
e2e: {},
|
||||
}
|
||||
`,
|
||||
output: dedent`
|
||||
export default {
|
||||
e2e: {},
|
||||
projectId: "abc1234",
|
||||
};
|
||||
`,
|
||||
},
|
||||
],
|
||||
})
|
||||
68
packages/config/test/ast-utils/addToCypressConfig.spec.ts
Normal file
68
packages/config/test/ast-utils/addToCypressConfig.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import proxyquire from 'proxyquire'
|
||||
import fsExtra from 'fs-extra'
|
||||
import sinon from 'sinon'
|
||||
import path from 'path'
|
||||
import { expect } from 'chai'
|
||||
import dedent from 'dedent'
|
||||
|
||||
const stub = sinon.stub()
|
||||
|
||||
beforeEach(() => {
|
||||
stub.reset()
|
||||
})
|
||||
|
||||
const { addTestingTypeToCypressConfig } = proxyquire('../../src/ast-utils/addToCypressConfig', {
|
||||
'fs-extra': {
|
||||
...fsExtra,
|
||||
writeFile: stub,
|
||||
},
|
||||
}) as typeof import('../../src/ast-utils/addToCypressConfig')
|
||||
|
||||
describe('addToCypressConfig', () => {
|
||||
it('will create a file if the file is empty', async () => {
|
||||
const result = await addTestingTypeToCypressConfig({
|
||||
filePath: path.join(__dirname, '../__fixtures__/empty.config.ts'),
|
||||
info: {
|
||||
testingType: 'e2e',
|
||||
},
|
||||
})
|
||||
|
||||
expect(stub.firstCall.args[1].trim()).to.eq(dedent`
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
});
|
||||
`)
|
||||
|
||||
expect(result.result).to.eq('ADDED')
|
||||
})
|
||||
|
||||
it('will error if we are unable to add to the config', async () => {
|
||||
const result = await addTestingTypeToCypressConfig({
|
||||
filePath: path.join(__dirname, '../__fixtures__/invalid.config.ts'),
|
||||
info: {
|
||||
testingType: 'e2e',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.result).to.eq('NEEDS_MERGE')
|
||||
expect(result.error.message).to.eq('Unable to automerge with the config file')
|
||||
})
|
||||
|
||||
it('will error if the key we are adding already exists', async () => {
|
||||
const result = await addTestingTypeToCypressConfig({
|
||||
filePath: path.join(__dirname, '../__fixtures__/has-e2e.config.ts'),
|
||||
info: {
|
||||
testingType: 'e2e',
|
||||
},
|
||||
})
|
||||
|
||||
expect(result.result).to.eq('NEEDS_MERGE')
|
||||
expect(result.error.message).to.eq('Unable to automerge with the config file')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import path from 'path'
|
||||
import pluginTester from 'babel-plugin-tester'
|
||||
|
||||
import { addToCypressConfigPlugin } from '../../src/ast-utils/addToCypressConfigPlugin'
|
||||
import { addE2EDefinition } from '../../src/ast-utils/astConfigHelpers'
|
||||
|
||||
pluginTester({
|
||||
pluginName: 'addToCypressConfigPlugin: e2e',
|
||||
plugin: () => {
|
||||
return addToCypressConfigPlugin(
|
||||
addE2EDefinition(),
|
||||
{ shouldThrow: false },
|
||||
)
|
||||
},
|
||||
fixtures: path.join(__dirname, '..', '__babel_fixtures__', 'adding-e2e'),
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import pluginTester from 'babel-plugin-tester'
|
||||
import path from 'path'
|
||||
|
||||
import { addToCypressConfigPlugin } from '../../src/ast-utils/addToCypressConfigPlugin'
|
||||
import { addComponentDefinition } from '../../src/ast-utils/astConfigHelpers'
|
||||
|
||||
pluginTester({
|
||||
pluginName: 'addToCypressConfigPlugin: component',
|
||||
plugin: () => {
|
||||
return addToCypressConfigPlugin(
|
||||
addComponentDefinition({ testingType: 'component', framework: 'react', bundler: 'webpack' }),
|
||||
{ shouldThrow: false },
|
||||
)
|
||||
},
|
||||
fixtures: path.join(__dirname, '..', '__babel_fixtures__', 'adding-component'),
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import snapshot from 'snap-shot-it'
|
||||
import sinon from 'sinon'
|
||||
import sinonChai from 'sinon-chai'
|
||||
|
||||
import * as configUtil from '../../src/index'
|
||||
import * as configUtil from '../src/index'
|
||||
|
||||
chai.use(sinonChai)
|
||||
const { expect } = chai
|
||||
@@ -1,7 +1,7 @@
|
||||
import snapshot from 'snap-shot-it'
|
||||
import { expect } from 'chai'
|
||||
|
||||
import * as validation from '../../src/validation'
|
||||
import * as validation from '../src/validation'
|
||||
|
||||
describe('config/lib/validation', () => {
|
||||
const mockKey = 'mockConfigKey'
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
ProjectDataSource,
|
||||
WizardDataSource,
|
||||
BrowserDataSource,
|
||||
StorybookDataSource,
|
||||
CloudDataSource,
|
||||
EnvDataSource,
|
||||
HtmlDataSource,
|
||||
@@ -34,7 +33,7 @@ import type { IncomingHttpHeaders, Server } from 'http'
|
||||
import type { AddressInfo } from 'net'
|
||||
import type { App as ElectronApp } from 'electron'
|
||||
import { VersionsDataSource } from './sources/VersionsDataSource'
|
||||
import type { SocketIOServer } from '@packages/socket'
|
||||
import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
|
||||
import { globalPubSub } from '.'
|
||||
import { InjectedConfigApi, ProjectLifecycleManager } from './data/ProjectLifecycleManager'
|
||||
import type { CypressError } from '@packages/errors'
|
||||
@@ -170,11 +169,6 @@ export class DataContext {
|
||||
return new WizardDataSource(this)
|
||||
}
|
||||
|
||||
@cached
|
||||
get storybook () {
|
||||
return new StorybookDataSource(this)
|
||||
}
|
||||
|
||||
get wizardData () {
|
||||
return this.coreData.wizard
|
||||
}
|
||||
@@ -238,7 +232,9 @@ export class DataContext {
|
||||
setAppSocketServer (socketServer: SocketIOServer | undefined) {
|
||||
this.update((d) => {
|
||||
d.servers.appSocketServer?.disconnectSockets(true)
|
||||
d.servers.appSocketNamespace?.disconnectSockets(true)
|
||||
d.servers.appSocketServer = socketServer
|
||||
d.servers.appSocketNamespace = socketServer?.of('/data-context')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,7 +245,7 @@ export class DataContext {
|
||||
})
|
||||
}
|
||||
|
||||
setGqlSocketServer (socketServer: SocketIOServer | undefined) {
|
||||
setGqlSocketServer (socketServer: SocketIONamespace | undefined) {
|
||||
this.update((d) => {
|
||||
d.servers.gqlSocketServer?.disconnectSockets(true)
|
||||
d.servers.gqlSocketServer = socketServer
|
||||
@@ -406,9 +402,7 @@ export class DataContext {
|
||||
await this.lifecycleManager.initializeRunMode(this.coreData.currentTestingType)
|
||||
} else if (this._config.mode === 'open') {
|
||||
await this.initializeOpenMode()
|
||||
if (this.coreData.currentProject && this.coreData.currentTestingType && await this.lifecycleManager.waitForInitializeSuccess()) {
|
||||
this.lifecycleManager.setAndLoadCurrentTestingType(this.coreData.currentTestingType)
|
||||
}
|
||||
await this.lifecycleManager.initializeOpenMode(this.coreData.currentTestingType)
|
||||
} else {
|
||||
throw new Error(`Missing DataContext config "mode" setting, expected run | open`)
|
||||
}
|
||||
@@ -433,6 +427,6 @@ export class DataContext {
|
||||
// load projects from cache on start
|
||||
toAwait.push(this.actions.project.loadProjects())
|
||||
|
||||
return Promise.all(toAwait)
|
||||
await Promise.all(toAwait)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,14 @@ abstract class DataEmitterEvents {
|
||||
this._emit('devChange')
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when cypress.config is re-executed and we'd like to
|
||||
* either re-run a spec or update something in the App UI.
|
||||
*/
|
||||
configChange () {
|
||||
this._emit('configChange')
|
||||
}
|
||||
|
||||
/**
|
||||
* Emitted when we have a notification from the cloud to refresh the data
|
||||
*/
|
||||
@@ -72,16 +80,16 @@ export class DataEmitterActions extends DataEmitterEvents {
|
||||
* Broadcasts a signal to the "app" via Socket.io, typically used to trigger
|
||||
* a re-query of data on the frontend
|
||||
*/
|
||||
toApp (...args: any[]) {
|
||||
this.ctx.coreData.servers.appSocketServer?.emit('graphql-refresh')
|
||||
toApp () {
|
||||
this.ctx.coreData.servers.appSocketNamespace?.emit('graphql-refetch')
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts a signal to the "launchpad" (Electron GUI) via Socket.io,
|
||||
* typically used to trigger a re-query of data on the frontend
|
||||
*/
|
||||
toLaunchpad (...args: any[]) {
|
||||
this.ctx.coreData.servers.gqlSocketServer?.emit('graphql-refresh')
|
||||
toLaunchpad () {
|
||||
this.ctx.coreData.servers.gqlSocketServer?.emit('graphql-refetch')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,9 +97,9 @@ export class DataEmitterActions extends DataEmitterEvents {
|
||||
* source, and respond with the data before the initial hit was able to resolve
|
||||
*/
|
||||
notifyClientRefetch (target: 'app' | 'launchpad', operation: string, field: string, variables: any) {
|
||||
const server = target === 'app' ? this.ctx.coreData.servers.appSocketServer : this.ctx.coreData.servers.gqlSocketServer
|
||||
const server = target === 'app' ? this.ctx.coreData.servers.appSocketNamespace : this.ctx.coreData.servers.gqlSocketServer
|
||||
|
||||
server?.emit('graphql-refresh', {
|
||||
server?.emit('graphql-refetch', {
|
||||
field,
|
||||
operation,
|
||||
variables,
|
||||
|
||||
@@ -8,7 +8,19 @@ import assert from 'assert'
|
||||
export class FileActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async writeFileInProject (relativePath: string, data: any) {
|
||||
readFileInProject (relativePath: string): string {
|
||||
if (!this.ctx.currentProject) {
|
||||
throw new Error(`Cannot write file in project without active project`)
|
||||
}
|
||||
|
||||
const filePath = path.join(this.ctx.currentProject, relativePath)
|
||||
|
||||
this.ctx.fs.ensureDirSync(path.dirname(filePath))
|
||||
|
||||
return this.ctx.fs.readFileSync(filePath, 'utf-8')
|
||||
}
|
||||
|
||||
writeFileInProject (relativePath: string, data: any) {
|
||||
if (!this.ctx.currentProject) {
|
||||
throw new Error(`Cannot write file in project without active project`)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class MigrationActions {
|
||||
|
||||
await this.initializeFlags()
|
||||
|
||||
const legacyConfigFileExist = await this.ctx.lifecycleManager.checkIfLegacyConfigFileExist()
|
||||
const legacyConfigFileExist = this.ctx.migration.legacyConfigFileExists()
|
||||
const filteredSteps = await getStepsForMigration(this.ctx.currentProject, legacyConfigForMigration, Boolean(legacyConfigFileExist))
|
||||
|
||||
this.ctx.update((coreData) => {
|
||||
@@ -203,7 +203,7 @@ export class MigrationActions {
|
||||
}
|
||||
|
||||
get configFileNameAfterMigration () {
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
return this.ctx.migration.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
}
|
||||
|
||||
async createConfigFile () {
|
||||
@@ -215,7 +215,7 @@ export class MigrationActions {
|
||||
throw error
|
||||
})
|
||||
|
||||
await this.ctx.actions.file.removeFileInProject(this.ctx.lifecycleManager.legacyConfigFile).catch((error) => {
|
||||
await this.ctx.actions.file.removeFileInProject(this.ctx.migration.legacyConfigFile).catch((error) => {
|
||||
throw error
|
||||
})
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface ProjectApiShape {
|
||||
clearAllProjectPreferences(): Promise<unknown>
|
||||
closeActiveProject(shouldCloseBrowser?: boolean): Promise<unknown>
|
||||
getConfig(): ReceivedCypressOptions | undefined
|
||||
getCurrentBrowser: () => Cypress.Browser | undefined
|
||||
getCurrentProjectSavedState(): {} | undefined
|
||||
setPromptShown(slug: string): void
|
||||
getDevServer (): {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { CodeLanguageEnum, NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import { CODE_LANGUAGES } from '@packages/types'
|
||||
import { detect, WIZARD_FRAMEWORKS, WIZARD_BUNDLERS, commandsFileBody, supportFileComponent, supportFileE2E } from '@packages/scaffold-config'
|
||||
import assert from 'assert'
|
||||
import dedent from 'dedent'
|
||||
import path from 'path'
|
||||
import Debug from 'debug'
|
||||
import fs from 'fs-extra'
|
||||
|
||||
const debug = Debug('cypress:data-context:wizard-actions')
|
||||
|
||||
import type { DataContext } from '..'
|
||||
import { addTestingTypeToCypressConfig, AddTestingTypeToCypressConfigOptions } from '@packages/config'
|
||||
|
||||
export class WizardActions {
|
||||
constructor (private ctx: DataContext) {}
|
||||
@@ -69,16 +69,11 @@ export class WizardActions {
|
||||
|
||||
async completeSetup () {
|
||||
debug('completeSetup')
|
||||
// wait for the config to be initialized if it is not yet
|
||||
// before returning. This should not penalize users but
|
||||
// allow for tests, too fast for this last step to pass.
|
||||
// NOTE: if the config is already initialized, this will be instant
|
||||
await this.ctx.lifecycleManager.initializeConfig()
|
||||
this.ctx.update((d) => {
|
||||
d.scaffoldedFiles = null
|
||||
})
|
||||
|
||||
this.ctx.lifecycleManager.loadTestingType()
|
||||
await this.ctx.lifecycleManager.refreshLifecycle()
|
||||
}
|
||||
|
||||
/// reset wizard status, useful for when changing to a new project
|
||||
@@ -142,14 +137,16 @@ export class WizardActions {
|
||||
async scaffoldTestingType () {
|
||||
const { currentTestingType, wizard: { chosenLanguage } } = this.ctx.coreData
|
||||
|
||||
// TODO: tgriesser, clean this up as part of UNIFY-1256
|
||||
if (!currentTestingType || !chosenLanguage) {
|
||||
return
|
||||
}
|
||||
assert(currentTestingType && chosenLanguage, 'currentTestingType & chosenLanguage are required')
|
||||
|
||||
switch (currentTestingType) {
|
||||
case 'e2e': {
|
||||
this.ctx.coreData.scaffoldedFiles = await this.scaffoldE2E()
|
||||
const scaffoldedFiles = await this.scaffoldE2E()
|
||||
|
||||
this.ctx.update((d) => {
|
||||
d.scaffoldedFiles = scaffoldedFiles
|
||||
})
|
||||
|
||||
this.ctx.lifecycleManager.refreshMetaState()
|
||||
this.ctx.actions.project.setForceReconfigureProjectByTestingType({ forceReconfigureProject: false, testingType: 'e2e' })
|
||||
|
||||
@@ -158,11 +155,14 @@ export class WizardActions {
|
||||
case 'component': {
|
||||
const { chosenBundler, chosenFramework } = this.ctx.coreData.wizard
|
||||
|
||||
if (!chosenBundler || !chosenFramework) {
|
||||
return
|
||||
}
|
||||
assert(chosenBundler && chosenFramework, 'chosenBundler & chosenFramework are required')
|
||||
|
||||
const scaffoldedFiles = await this.scaffoldComponent()
|
||||
|
||||
this.ctx.update((d) => {
|
||||
d.scaffoldedFiles = scaffoldedFiles
|
||||
})
|
||||
|
||||
this.ctx.coreData.scaffoldedFiles = await this.scaffoldComponent()
|
||||
this.ctx.lifecycleManager.refreshMetaState()
|
||||
this.ctx.actions.project.setForceReconfigureProjectByTestingType({ forceReconfigureProject: false, testingType: 'component' })
|
||||
|
||||
@@ -238,57 +238,56 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
private configCode (testingType: 'e2e' | 'component', language: CodeLanguageEnum) {
|
||||
if (testingType === 'component') {
|
||||
const chosenLanguage = CODE_LANGUAGES.find((f) => f.type === language)
|
||||
|
||||
const { chosenBundler, chosenFramework } = this.ctx.coreData.wizard
|
||||
|
||||
assert(chosenFramework && chosenLanguage && chosenBundler && this.ctx.currentProject)
|
||||
|
||||
return chosenFramework.createCypressConfig({
|
||||
language: chosenLanguage.type,
|
||||
bundler: chosenBundler.type,
|
||||
framework: chosenFramework.configFramework,
|
||||
projectRoot: this.ctx.currentProject,
|
||||
})
|
||||
}
|
||||
|
||||
return this.wizardGetConfigCodeE2E(language)
|
||||
}
|
||||
|
||||
private async scaffoldConfig (testingType: 'e2e' | 'component'): Promise<NexusGenObjects['ScaffoldedFile']> {
|
||||
debug('scaffoldConfig')
|
||||
|
||||
if (this.ctx.lifecycleManager.metaState.hasValidConfigFile) {
|
||||
const { ext } = path.parse(this.ctx.lifecycleManager.configFilePath)
|
||||
const foundLanguage = ext === '.ts' ? 'ts' : 'js'
|
||||
const configCode = this.configCode(testingType, foundLanguage)
|
||||
|
||||
return {
|
||||
status: 'changes',
|
||||
description: 'Merge this code with your existing config file.',
|
||||
file: {
|
||||
absolute: this.ctx.lifecycleManager.configFilePath,
|
||||
contents: configCode,
|
||||
},
|
||||
}
|
||||
if (!this.ctx.lifecycleManager.metaState.hasValidConfigFile) {
|
||||
this.ctx.lifecycleManager.setConfigFilePath(`cypress.config.${this.ctx.coreData.wizard.chosenLanguage}`)
|
||||
}
|
||||
|
||||
const configCode = this.configCode(testingType, this.ctx.coreData.wizard.chosenLanguage)
|
||||
const configFilePath = this.ctx.lifecycleManager.configFilePath
|
||||
const testingTypeInfo: AddTestingTypeToCypressConfigOptions['info'] = testingType === 'e2e' ? {
|
||||
testingType: 'e2e',
|
||||
} : {
|
||||
testingType: 'component',
|
||||
bundler: this.ctx.coreData.wizard.chosenBundler?.package ?? 'webpack',
|
||||
framework: this.ctx.coreData.wizard.chosenFramework?.configFramework,
|
||||
}
|
||||
|
||||
// only do this if config file doesn't exist
|
||||
this.ctx.lifecycleManager.setConfigFilePath(`cypress.config.${this.ctx.coreData.wizard.chosenLanguage}`)
|
||||
const result = await addTestingTypeToCypressConfig({
|
||||
filePath: configFilePath,
|
||||
info: testingTypeInfo,
|
||||
})
|
||||
|
||||
const description = (testingType === 'e2e')
|
||||
? 'The Cypress config file for E2E testing.'
|
||||
: 'The Cypress config file where the component testing dev server is configured.'
|
||||
|
||||
return this.scaffoldFile(
|
||||
this.ctx.lifecycleManager.configFilePath,
|
||||
configCode,
|
||||
description,
|
||||
)
|
||||
const descriptions = {
|
||||
ADDED: description,
|
||||
MERGED: `Added ${testingType} to the Cypress config file.`,
|
||||
CHANGES: 'Merge this code with your existing config file.',
|
||||
}
|
||||
|
||||
if (result.result === 'ADDED' || result.result === 'MERGED') {
|
||||
return {
|
||||
status: 'valid',
|
||||
description: descriptions[result.result],
|
||||
file: {
|
||||
absolute: configFilePath,
|
||||
contents: await fs.readFile(configFilePath, 'utf8'),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'changes',
|
||||
description: descriptions.CHANGES,
|
||||
file: {
|
||||
absolute: this.ctx.lifecycleManager.configFilePath,
|
||||
contents: result.codeToMerge ?? '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
private async scaffoldFixtures (): Promise<NexusGenObjects['ScaffoldedFile']> {
|
||||
@@ -316,24 +315,11 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
private wizardGetConfigCodeE2E (lang: CodeLanguageEnum): string {
|
||||
const codeBlocks: string[] = []
|
||||
|
||||
codeBlocks.push(lang === 'ts' ? `import { defineConfig } from 'cypress'` : `const { defineConfig } = require('cypress')`)
|
||||
codeBlocks.push('')
|
||||
codeBlocks.push(lang === 'ts' ? `export default defineConfig({` : `module.exports = defineConfig({`)
|
||||
codeBlocks.push(` ${E2E_SCAFFOLD_BODY.replace(/\n/g, '\n ')}`)
|
||||
|
||||
codeBlocks.push('})\n')
|
||||
|
||||
return codeBlocks.join('\n')
|
||||
}
|
||||
|
||||
private async scaffoldComponentIndexHtml (chosenFramework: typeof WIZARD_FRAMEWORKS[number]): Promise<NexusGenObjects['ScaffoldedFile']> {
|
||||
await this.ensureDir('component')
|
||||
|
||||
const componentIndexHtmlPath = path.join(this.projectRoot, 'cypress', 'support', 'component-index.html')
|
||||
|
||||
await this.ensureDir('support')
|
||||
|
||||
return this.scaffoldFile(
|
||||
componentIndexHtmlPath,
|
||||
chosenFramework.componentIndexHtml(),
|
||||
@@ -377,19 +363,11 @@ export class WizardActions {
|
||||
}
|
||||
}
|
||||
|
||||
private ensureDir (type: 'component' | 'e2e' | 'fixtures') {
|
||||
private ensureDir (type: 'e2e' | 'fixtures' | 'support') {
|
||||
return this.ctx.fs.ensureDir(path.join(this.projectRoot, 'cypress', type))
|
||||
}
|
||||
}
|
||||
|
||||
const E2E_SCAFFOLD_BODY = dedent`
|
||||
e2e: {
|
||||
setupNodeEvents(on, config) {
|
||||
// implement node event listeners here
|
||||
},
|
||||
},
|
||||
`
|
||||
|
||||
const FIXTURE_DATA = {
|
||||
'name': 'Using fixtures to represent data',
|
||||
'email': 'hello@cypress.io',
|
||||
|
||||
@@ -26,7 +26,7 @@ type ProjectConfigManagerOptions = {
|
||||
onError: (cypressError: CypressError, title?: string | undefined) => void
|
||||
onInitialConfigLoaded: (initialConfig: Cypress.ConfigOptions) => void
|
||||
onFinalConfigLoaded: (finalConfig: FullConfig) => Promise<void>
|
||||
refreshLifecycle: () => Promise<boolean>
|
||||
refreshLifecycle: () => Promise<void>
|
||||
}
|
||||
|
||||
type ConfigManagerState = 'pending' | 'loadingConfig' | 'loadedConfig' | 'loadingNodeEvents' | 'ready' | 'errored'
|
||||
|
||||
@@ -115,18 +115,6 @@ export class ProjectLifecycleManager {
|
||||
return Object.freeze(this._projectMetaState)
|
||||
}
|
||||
|
||||
get legacyJsonPath () {
|
||||
return path.join(this.configFilePath, this.legacyConfigFile)
|
||||
}
|
||||
|
||||
get legacyConfigFile () {
|
||||
if (this.ctx.modeOptions.configFile && this.ctx.modeOptions.configFile.endsWith('.json')) {
|
||||
return this.ctx.modeOptions.configFile
|
||||
}
|
||||
|
||||
return 'cypress.json'
|
||||
}
|
||||
|
||||
get configFile () {
|
||||
return this.ctx.modeOptions.configFile ?? (this._configManager?.configFilePath && path.basename(this._configManager.configFilePath)) ?? 'cypress.config.js'
|
||||
}
|
||||
@@ -188,12 +176,6 @@ export class ProjectLifecycleManager {
|
||||
return this.metaState.hasTypescript ? 'ts' : 'js'
|
||||
}
|
||||
|
||||
async checkIfLegacyConfigFileExist () {
|
||||
const legacyConfigFileExist = await this.ctx.file.checkIfFileExists(this.legacyConfigFile)
|
||||
|
||||
return Boolean(legacyConfigFileExist)
|
||||
}
|
||||
|
||||
clearCurrentProject () {
|
||||
this.resetInternalState()
|
||||
this._initializedProject = undefined
|
||||
@@ -234,14 +216,6 @@ export class ProjectLifecycleManager {
|
||||
onInitialConfigLoaded: (initialConfig: Cypress.ConfigOptions) => {
|
||||
this._cachedInitialConfig = initialConfig
|
||||
|
||||
if (this.ctx.coreData.scaffoldedFiles) {
|
||||
this.ctx.coreData.scaffoldedFiles.filter((f) => {
|
||||
if (f.file.absolute === this.configFilePath && f.status !== 'valid') {
|
||||
f.status = 'valid'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.ctx.emitter.toLaunchpad()
|
||||
},
|
||||
onFinalConfigLoaded: async (finalConfig: FullConfig) => {
|
||||
@@ -267,6 +241,7 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
|
||||
this._pendingInitialize?.resolve(finalConfig)
|
||||
this.ctx.emitter.configChange()
|
||||
},
|
||||
refreshLifecycle: async () => this.refreshLifecycle(),
|
||||
})
|
||||
@@ -309,24 +284,23 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
}
|
||||
|
||||
async refreshLifecycle () {
|
||||
assert(this._projectRoot, 'Cannot reload config without a project root')
|
||||
assert(this._configManager, 'Cannot reload config without a config manager')
|
||||
|
||||
if (this.readyToInitialize(this._projectRoot)) {
|
||||
this._configManager.resetLoadingState()
|
||||
await this.initializeConfig()
|
||||
|
||||
if (this._currentTestingType && this.isTestingTypeConfigured(this._currentTestingType)) {
|
||||
this._configManager.loadTestingType()
|
||||
} else {
|
||||
this.setAndLoadCurrentTestingType(null)
|
||||
}
|
||||
|
||||
return true
|
||||
async refreshLifecycle (): Promise<void> {
|
||||
if (!this._projectRoot || !this._configManager || !this.readyToInitialize(this._projectRoot)) {
|
||||
return
|
||||
}
|
||||
|
||||
return false
|
||||
this._configManager.resetLoadingState()
|
||||
|
||||
// Emit here so that the user gets the impression that we're loading rather than waiting for a full refresh of the config for an update
|
||||
this.ctx.emitter.toLaunchpad()
|
||||
|
||||
await this.initializeConfig()
|
||||
|
||||
if (this._currentTestingType && this.isTestingTypeConfigured(this._currentTestingType)) {
|
||||
this._configManager.loadTestingType()
|
||||
} else {
|
||||
this.setAndLoadCurrentTestingType(null)
|
||||
}
|
||||
}
|
||||
|
||||
async waitForInitializeSuccess (): Promise<boolean> {
|
||||
@@ -395,6 +369,8 @@ export class ProjectLifecycleManager {
|
||||
s.packageManager = packageManagerUsed
|
||||
})
|
||||
|
||||
this.verifyProjectRoot(projectRoot)
|
||||
|
||||
if (this.readyToInitialize(this._projectRoot)) {
|
||||
this._configManager.initializeConfig().catch(this.onLoadError)
|
||||
}
|
||||
@@ -407,12 +383,10 @@ export class ProjectLifecycleManager {
|
||||
* @param projectRoot the project's root
|
||||
* @returns true if we can initialize and false if not
|
||||
*/
|
||||
readyToInitialize (projectRoot: string): boolean {
|
||||
this.verifyProjectRoot(projectRoot)
|
||||
|
||||
private readyToInitialize (projectRoot: string): boolean {
|
||||
const { needsCypressJsonMigration } = this.refreshMetaState()
|
||||
|
||||
const legacyConfigPath = path.join(projectRoot, this.legacyConfigFile)
|
||||
const legacyConfigPath = path.join(projectRoot, this.ctx.migration.legacyConfigFile)
|
||||
|
||||
if (needsCypressJsonMigration && !this.ctx.isRunMode && this.ctx.fs.existsSync(legacyConfigPath)) {
|
||||
this.legacyMigration(legacyConfigPath).catch(this.onLoadError)
|
||||
@@ -427,7 +401,7 @@ export class ProjectLifecycleManager {
|
||||
return this.metaState.hasValidConfigFile
|
||||
}
|
||||
|
||||
async legacyMigration (legacyConfigPath: string) {
|
||||
private async legacyMigration (legacyConfigPath: string) {
|
||||
try {
|
||||
// we run the legacy plugins/index.js in a child process
|
||||
// and mutate the config based on the return value for migration
|
||||
@@ -500,18 +474,6 @@ export class ProjectLifecycleManager {
|
||||
}
|
||||
}
|
||||
|
||||
loadTestingType () {
|
||||
assert(this._configManager, 'Cannot load a testing type without a config manager')
|
||||
|
||||
this._configManager.loadTestingType()
|
||||
}
|
||||
|
||||
scaffoldFilesIfNecessary () {
|
||||
if (this._currentTestingType && this._projectMetaState.hasValidConfigFile && !this.isTestingTypeConfigured(this._currentTestingType) && !this.ctx.isRunMode) {
|
||||
this.ctx.actions.wizard.scaffoldTestingType().catch(this.onLoadError)
|
||||
}
|
||||
}
|
||||
|
||||
private resetInternalState () {
|
||||
if (this._configManager) {
|
||||
this._configManager.destroy()
|
||||
@@ -541,12 +503,6 @@ export class ProjectLifecycleManager {
|
||||
return this._configManager.getConfigFileContents()
|
||||
}
|
||||
|
||||
async loadCypressEnvFile () {
|
||||
assert(this._configManager, 'Cannot load a cypress env file without a config manager')
|
||||
|
||||
return this._configManager.loadCypressEnvFile()
|
||||
}
|
||||
|
||||
reinitializeCypress () {
|
||||
resetPluginHandlers()
|
||||
this.resetInternalState()
|
||||
@@ -585,7 +541,7 @@ export class ProjectLifecycleManager {
|
||||
const configFile = this.ctx.modeOptions.configFile
|
||||
const metaState: ProjectMetaState = {
|
||||
...PROJECT_META_STATE,
|
||||
hasLegacyCypressJson: fs.existsSync(this._pathToFile(this.legacyConfigFile)),
|
||||
hasLegacyCypressJson: this.ctx.migration.legacyConfigFileExists(),
|
||||
hasCypressEnvFile: fs.existsSync(this._pathToFile('cypress.env.json')),
|
||||
}
|
||||
|
||||
@@ -703,10 +659,10 @@ export class ProjectLifecycleManager {
|
||||
return true
|
||||
}
|
||||
|
||||
async needsCypressJsonMigration () {
|
||||
const legacyConfigFileExist = await this.checkIfLegacyConfigFileExist()
|
||||
|
||||
return this.metaState.needsCypressJsonMigration && Boolean(legacyConfigFileExist)
|
||||
async initializeOpenMode (testingType: TestingType | null) {
|
||||
if (this._projectRoot && testingType && await this.waitForInitializeSuccess()) {
|
||||
this.setAndLoadCurrentTestingType(testingType)
|
||||
}
|
||||
}
|
||||
|
||||
async initializeRunMode (testingType: TestingType | null) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { WIZARD_BUNDLERS, WIZARD_FRAMEWORKS } from '@packages/scaffold-conf
|
||||
import type { NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen'
|
||||
import type { App, BrowserWindow } from 'electron'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import type { SocketIOServer } from '@packages/socket'
|
||||
import type { SocketIONamespace, SocketIOServer } from '@packages/socket'
|
||||
import type { Server } from 'http'
|
||||
import type { ErrorWrapperSource } from '@packages/errors'
|
||||
import type { GitDataSource, LegacyCypressConfigJson } from '../sources'
|
||||
@@ -115,9 +115,10 @@ export interface CoreDataShape {
|
||||
appServer?: Maybe<Server>
|
||||
appServerPort?: Maybe<number>
|
||||
appSocketServer?: Maybe<SocketIOServer>
|
||||
appSocketNamespace?: Maybe<SocketIONamespace>
|
||||
gqlServer?: Maybe<Server>
|
||||
gqlServerPort?: Maybe<number>
|
||||
gqlSocketServer?: Maybe<SocketIOServer>
|
||||
gqlSocketServer?: Maybe<SocketIONamespace>
|
||||
}
|
||||
hasInitializedMode: 'run' | 'open' | null
|
||||
baseError: ErrorWrapperSource | null
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
import type { DataContext } from '../DataContext'
|
||||
import { getPathToDist, resolveFromPackages } from '@packages/resolve-dist'
|
||||
import _ from 'lodash'
|
||||
|
||||
const PATH_TO_NON_PROXIED_ERROR = resolveFromPackages('server', 'lib', 'html', 'non_proxied_error.html')
|
||||
|
||||
@@ -41,8 +42,32 @@ export class HtmlDataSource {
|
||||
throw err
|
||||
}
|
||||
|
||||
getUrlsFromLegacyProjectBase (cfg: any) {
|
||||
const keys = [
|
||||
'baseUrl',
|
||||
'browserUrl',
|
||||
'port',
|
||||
'proxyServer',
|
||||
'proxyUrl',
|
||||
'remote',
|
||||
'testingType',
|
||||
'componentTesting',
|
||||
'reporterUrl',
|
||||
'xhrUrl',
|
||||
]
|
||||
|
||||
return _.pick(cfg, keys)
|
||||
}
|
||||
|
||||
async makeServeConfig () {
|
||||
const cfg = this.ctx._apis.projectApi.getConfig() ?? {} as any
|
||||
const fieldsFromLegacyCfg = this.getUrlsFromLegacyProjectBase(this.ctx._apis.projectApi.getConfig() ?? {})
|
||||
|
||||
const cfg = {
|
||||
...(await this.ctx.project.getConfig()),
|
||||
...fieldsFromLegacyCfg,
|
||||
}
|
||||
|
||||
cfg.browser = this.ctx._apis.projectApi.getCurrentBrowser()
|
||||
|
||||
return {
|
||||
projectName: this.ctx.lifecycleManager.projectTitle,
|
||||
|
||||
@@ -18,6 +18,7 @@ import _ from 'lodash'
|
||||
|
||||
import type { FilePart } from './migration/format'
|
||||
import Debug from 'debug'
|
||||
import path from 'path'
|
||||
|
||||
const debug = Debug('cypress:data-context:sources:MigrationDataSource')
|
||||
|
||||
@@ -61,6 +62,32 @@ export class MigrationDataSource {
|
||||
return this.ctx.coreData.migration.legacyConfigForMigration
|
||||
}
|
||||
|
||||
get legacyConfigFile () {
|
||||
if (this.ctx.modeOptions.configFile && this.ctx.modeOptions.configFile.endsWith('.json')) {
|
||||
return this.ctx.modeOptions.configFile
|
||||
}
|
||||
|
||||
return 'cypress.json'
|
||||
}
|
||||
|
||||
legacyConfigFileExists (): boolean {
|
||||
// If we aren't in a current project we definitely don't have a legacy config file
|
||||
if (!this.ctx.currentProject) {
|
||||
return false
|
||||
}
|
||||
|
||||
const configFilePath = path.isAbsolute(this.legacyConfigFile) ? this.legacyConfigFile : path.join(this.ctx.currentProject, this.legacyConfigFile)
|
||||
const legacyConfigFileExists = this.ctx.fs.existsSync(configFilePath)
|
||||
|
||||
return Boolean(legacyConfigFileExists)
|
||||
}
|
||||
|
||||
needsCypressJsonMigration (): boolean {
|
||||
const legacyConfigFileExists = this.legacyConfigFileExists()
|
||||
|
||||
return this.ctx.lifecycleManager.metaState.needsCypressJsonMigration && Boolean(legacyConfigFileExists)
|
||||
}
|
||||
|
||||
async getComponentTestingMigrationStatus () {
|
||||
debug('getComponentTestingMigrationStatus: start')
|
||||
if (!this.legacyConfig || !this.ctx.currentProject) {
|
||||
@@ -190,7 +217,7 @@ export class MigrationDataSource {
|
||||
}
|
||||
|
||||
get configFileNameAfterMigration () {
|
||||
return this.ctx.lifecycleManager.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
return this.legacyConfigFile.replace('.json', `.config.${this.ctx.lifecycleManager.fileExtensionToUse}`)
|
||||
}
|
||||
|
||||
private checkAndUpdateDuplicatedSpecs (specs: MigrationFile[]) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { StorybookInfo } from '@packages/types'
|
||||
import assert from 'assert'
|
||||
import * as path from 'path'
|
||||
import type { DataContext } from '..'
|
||||
|
||||
const STORYBOOK_FILES = [
|
||||
'main.js',
|
||||
'preview.js',
|
||||
'preview-head.html',
|
||||
'preview-body.html',
|
||||
]
|
||||
|
||||
export class StorybookDataSource {
|
||||
constructor (private ctx: DataContext) {}
|
||||
|
||||
async loadStorybookInfo () {
|
||||
assert(this.ctx.currentProject)
|
||||
|
||||
return this.storybookInfoLoader.load(this.ctx.currentProject)
|
||||
}
|
||||
|
||||
private storybookInfoLoader = this.ctx.loader<string, StorybookInfo | null>((projectRoots) => this.batchStorybookInfo(projectRoots))
|
||||
|
||||
private batchStorybookInfo (projectRoots: readonly string[]) {
|
||||
return Promise.all(projectRoots.map((projectRoot) => this.detectStorybook(projectRoot)))
|
||||
}
|
||||
|
||||
private async detectStorybook (projectRoot: string): Promise<StorybookInfo | null> {
|
||||
const storybookRoot = path.join(projectRoot, '.storybook')
|
||||
const storybookInfo: StorybookInfo = {
|
||||
storybookRoot,
|
||||
files: [],
|
||||
}
|
||||
|
||||
try {
|
||||
await this.ctx.fs.access(storybookRoot, this.ctx.fs.constants.F_OK)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
for (const fileName of STORYBOOK_FILES) {
|
||||
try {
|
||||
const absolute = path.join(storybookRoot, fileName)
|
||||
const file = {
|
||||
name: fileName,
|
||||
relative: path.relative(projectRoot, absolute),
|
||||
absolute,
|
||||
content: await this.ctx.fs.readFile(absolute, 'utf-8'),
|
||||
}
|
||||
|
||||
storybookInfo.files.push(file)
|
||||
} catch (e) {
|
||||
// eslint-disable-line no-empty
|
||||
}
|
||||
}
|
||||
|
||||
return storybookInfo
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,6 @@ export class WizardDataSource {
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Storybook support.
|
||||
// const storybookInfo = await this.ctx.storybook.loadStorybookInfo()
|
||||
|
||||
// if (storybookInfo && this.chosenFramework.storybookDep) {
|
||||
// packages.push(this.chosenFramework.storybookDep)
|
||||
// }
|
||||
|
||||
return packages
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ export * from './HtmlDataSource'
|
||||
export * from './MigrationDataSource'
|
||||
export * from './ProjectDataSource'
|
||||
export * from './SettingsDataSource'
|
||||
export * from './StorybookDataSource'
|
||||
export * from './UtilDataSource'
|
||||
export * from './VersionsDataSource'
|
||||
export * from './WizardDataSource'
|
||||
|
||||
@@ -2,7 +2,6 @@ import chokidar from 'chokidar'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import globby from 'globby'
|
||||
import prettier from 'prettier'
|
||||
import type { TestingType } from '@packages/types'
|
||||
import { formatMigrationFile } from './format'
|
||||
import { substitute } from './autoRename'
|
||||
@@ -448,11 +447,17 @@ export function getSpecPattern (cfg: LegacyCypressConfigJson, testType: TestingT
|
||||
return specPattern
|
||||
}
|
||||
|
||||
export function formatConfig (config: string) {
|
||||
return prettier.format(config, {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf',
|
||||
parser: 'babel',
|
||||
})
|
||||
export function formatConfig (config: string): string {
|
||||
try {
|
||||
const prettier = require('prettier') as typeof import('prettier')
|
||||
|
||||
return prettier.format(config, {
|
||||
semi: false,
|
||||
singleQuote: true,
|
||||
endOfLine: 'lf',
|
||||
parser: 'babel',
|
||||
})
|
||||
} catch (e) {
|
||||
return config
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import './button.css';
|
||||
* Primary UI component for user interaction
|
||||
*/
|
||||
export default Button = ({ primary, backgroundColor, size, label, ...props }) => {
|
||||
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
|
||||
const mode = primary ? 'button--primary' : 'button--secondary';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={['storybook-button', `storybook-button--${size}`, mode].join(' ')}
|
||||
className={['button', `button--${size}`, mode].join(' ')}
|
||||
style={backgroundColor && { backgroundColor }}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -36,10 +36,10 @@ export default {
|
||||
props = reactive(props);
|
||||
return {
|
||||
classes: computed(() => ({
|
||||
'storybook-button': true,
|
||||
'storybook-button--primary': props.primary,
|
||||
'storybook-button--secondary': !props.primary,
|
||||
[`storybook-button--${props.size || 'medium'}`]: true,
|
||||
'button': true,
|
||||
'button--primary': props.primary,
|
||||
'button--secondary': !props.primary,
|
||||
[`button--${props.size || 'medium'}`]: true,
|
||||
})),
|
||||
style: computed(() => ({
|
||||
backgroundColor: props.backgroundColor,
|
||||
|
||||
@@ -142,25 +142,9 @@ describe('packagesToInstall', () => {
|
||||
expect(actual).to.eq('npm install -D nuxt@2 vue@2')
|
||||
})
|
||||
|
||||
it('pristine-with-e2e-testing-and-storybook', async () => {
|
||||
const ctx = createTestDataContext()
|
||||
|
||||
const projectPath = await scaffoldMigrationProject('pristine-with-e2e-testing-and-storybook')
|
||||
|
||||
ctx.update((coreData) => {
|
||||
coreData.currentProject = projectPath
|
||||
coreData.wizard.chosenFramework = findFramework('react')
|
||||
coreData.wizard.chosenBundler = findBundler('webpack')
|
||||
})
|
||||
|
||||
const actual = ctx.wizard.installDependenciesCommand()
|
||||
|
||||
expect(actual).to.eq('npm install -D webpack react')
|
||||
})
|
||||
|
||||
it('framework and bundler are undefined', async () => {
|
||||
const ctx = createTestDataContext()
|
||||
const projectPath = await scaffoldMigrationProject('pristine-with-e2e-testing-and-storybook')
|
||||
const projectPath = await scaffoldMigrationProject('pristine-with-e2e-testing')
|
||||
|
||||
ctx.update((coreData) => {
|
||||
// this should never happen!
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
const { _, $, Promise } = Cypress
|
||||
const {
|
||||
assertLogLength,
|
||||
getCommandLogWithText,
|
||||
findReactInstance,
|
||||
withMutableReporterState,
|
||||
clickCommandLog,
|
||||
attachListeners,
|
||||
shouldBeCalledWithCount,
|
||||
@@ -4762,23 +4759,10 @@ describe('mouse state', () => {
|
||||
|
||||
cy.get('input:first').click()
|
||||
|
||||
cy.wrap(null)
|
||||
.should(() => {
|
||||
spyTableName.resetHistory()
|
||||
spyTableData.resetHistory()
|
||||
|
||||
return withMutableReporterState(() => {
|
||||
const commandLogEl = getCommandLogWithText('click')
|
||||
|
||||
const reactCommandInstance = findReactInstance(commandLogEl.get(0))
|
||||
|
||||
reactCommandInstance.props.appState.isRunning = false
|
||||
|
||||
commandLogEl.find('.command-wrapper').click()
|
||||
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
})
|
||||
clickCommandLog('click')
|
||||
.then(() => {
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4788,26 +4772,13 @@ describe('mouse state', () => {
|
||||
|
||||
cy.get('input:first').dblclick()
|
||||
|
||||
cy.wrap(null, { timeout: 1000 })
|
||||
.should(() => {
|
||||
spyTableName.resetHistory()
|
||||
spyTableData.resetHistory()
|
||||
|
||||
return withMutableReporterState(() => {
|
||||
const commandLogEl = getCommandLogWithText('click')
|
||||
|
||||
const reactCommandInstance = findReactInstance(commandLogEl.get(0))
|
||||
|
||||
reactCommandInstance.props.appState.isRunning = false
|
||||
|
||||
commandLogEl.find('.command-wrapper').click()
|
||||
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
expect(spyTableData.lastCall.args[0]).property('8').includes({ 'Event Type': 'click' })
|
||||
expect(spyTableData.lastCall.args[0]).property('13').includes({ 'Event Type': 'click' })
|
||||
expect(spyTableData.lastCall.args[0]).property('14').includes({ 'Event Type': 'dblclick' })
|
||||
})
|
||||
clickCommandLog('click')
|
||||
.then(() => {
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
expect(spyTableData.lastCall.args[0]).property('8').includes({ 'Event Type': 'click' })
|
||||
expect(spyTableData.lastCall.args[0]).property('13').includes({ 'Event Type': 'click' })
|
||||
expect(spyTableData.lastCall.args[0]).property('14').includes({ 'Event Type': 'dblclick' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4817,24 +4788,11 @@ describe('mouse state', () => {
|
||||
|
||||
cy.get('input:first').rightclick()
|
||||
|
||||
cy.wrap(null)
|
||||
.should(() => {
|
||||
spyTableName.resetHistory()
|
||||
spyTableData.resetHistory()
|
||||
|
||||
return withMutableReporterState(() => {
|
||||
const commandLogEl = getCommandLogWithText('click')
|
||||
|
||||
const reactCommandInstance = findReactInstance(commandLogEl.get(0))
|
||||
|
||||
reactCommandInstance.props.appState.isRunning = false
|
||||
|
||||
commandLogEl.find('.command-wrapper').click()
|
||||
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
expect(spyTableData.lastCall.args[0]).property('8').includes({ 'Event Type': 'contextmenu' })
|
||||
})
|
||||
clickCommandLog('click')
|
||||
.then(() => {
|
||||
expect(spyTableName).calledWith('Mouse Events')
|
||||
expect(spyTableData).calledOnce
|
||||
expect(spyTableData.lastCall.args[0]).property('8').includes({ 'Event Type': 'contextmenu' })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -336,7 +336,7 @@ describe('src/cy/commands/actions/selectFile', () => {
|
||||
})
|
||||
|
||||
describe('errors', {
|
||||
defaultCommandTimeout: 50,
|
||||
defaultCommandTimeout: 500,
|
||||
}, () => {
|
||||
it('is a child command', (done) => {
|
||||
cy.on('fail', (err) => {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
const { _, $ } = Cypress
|
||||
const { Promise } = Cypress
|
||||
const {
|
||||
getCommandLogWithText,
|
||||
findReactInstance,
|
||||
withMutableReporterState,
|
||||
clickCommandLog,
|
||||
attachKeyListeners,
|
||||
keyEvents,
|
||||
trimInnerText,
|
||||
@@ -3027,23 +3025,14 @@ describe('src/cy/commands/actions/type - #type', () => {
|
||||
it('can print table of keys on click', () => {
|
||||
cy.get('input:first').type('foo')
|
||||
|
||||
const spyTableName = cy.spy(top.console, 'group')
|
||||
const spyTableData = cy.spy(top.console, 'table')
|
||||
|
||||
clickCommandLog('foo', 'message-text')
|
||||
.then(() => {
|
||||
return withMutableReporterState(() => {
|
||||
const spyTableName = cy.spy(top.console, 'group')
|
||||
const spyTableData = cy.spy(top.console, 'table')
|
||||
|
||||
const commandLogEl = getCommandLogWithText('foo', 'message-text')
|
||||
|
||||
const reactCommandInstance = findReactInstance(commandLogEl[0])
|
||||
|
||||
reactCommandInstance.props.appState.isRunning = false
|
||||
|
||||
$(commandLogEl).find('.command-wrapper').click()
|
||||
|
||||
expect(spyTableName.firstCall).calledWith('Mouse Events')
|
||||
expect(spyTableName.secondCall).calledWith('Keyboard Events')
|
||||
expect(spyTableData).calledTwice
|
||||
})
|
||||
expect(spyTableName.firstCall).calledWith('Mouse Events')
|
||||
expect(spyTableName.secondCall).calledWith('Keyboard Events')
|
||||
expect(spyTableData).calledTwice
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,6 +48,23 @@ describe('src/cy/commands/querying/within', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('can be chained off an alias', () => {
|
||||
const form = cy.$$('#by-name')
|
||||
|
||||
cy.get('#by-name').as('nameForm')
|
||||
.within(() => {})
|
||||
.then(($form) => {
|
||||
expect($form.get(0)).to.eq(form.get(0))
|
||||
})
|
||||
|
||||
cy.get('#by-name').as('nameForm')
|
||||
.within(() => {
|
||||
cy.get('input').should('be.visible')
|
||||
})
|
||||
|
||||
cy.get('@nameForm').should('be.visible')
|
||||
})
|
||||
|
||||
it('can call child commands after within on the same subject', () => {
|
||||
const input = cy.$$('#by-name input:first')
|
||||
|
||||
@@ -199,6 +216,20 @@ describe('src/cy/commands/querying/within', () => {
|
||||
expect(lastLog.get('snapshots')[0]).to.be.an('object')
|
||||
})
|
||||
})
|
||||
|
||||
it('provides additional information in console prop', () => {
|
||||
cy.get('div').within(() => {})
|
||||
.then(function () {
|
||||
const { lastLog } = this
|
||||
|
||||
const consoleProps = lastLog.get('consoleProps')()
|
||||
|
||||
expect(consoleProps).to.be.an('object')
|
||||
expect(consoleProps.Command).to.eq('within')
|
||||
expect(consoleProps.Yielded).to.not.be.null
|
||||
expect(consoleProps.Yielded).to.have.length(55)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('errors', {
|
||||
|
||||
@@ -638,4 +638,32 @@ describe('driver/src/cypress/error_utils', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
context('.getUnsupportedPlugin', () => {
|
||||
it('returns unsupported plugin if the error msg is the expected one', () => {
|
||||
const unsupportedPlugin = $errUtils.getUnsupportedPlugin({
|
||||
invocationDetails: {
|
||||
originalFile: 'node_modules/@cypress/code-coverage',
|
||||
},
|
||||
err: {
|
||||
message: 'glob pattern string required',
|
||||
},
|
||||
})
|
||||
|
||||
expect(unsupportedPlugin).to.eq('@cypress/code-coverage')
|
||||
})
|
||||
|
||||
it('returns null if the error msg is not the expected one', () => {
|
||||
const unsupportedPlugin = $errUtils.getUnsupportedPlugin({
|
||||
invocationDetails: {
|
||||
originalFile: 'node_modules/@cypress/code-coverage',
|
||||
},
|
||||
err: {
|
||||
message: 'random error msg',
|
||||
},
|
||||
})
|
||||
|
||||
expect(unsupportedPlugin).to.eq(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -114,13 +114,14 @@ const ensureCorrectHighlightPositions = (sel) => {
|
||||
const doc = els.content[0].ownerDocument
|
||||
|
||||
const contentHighlightCenter = [dims.content.x + dims.content.width / 2, dims.content.y + dims.content.height / 2]
|
||||
const highlightedEl = doc.elementFromPoint(...contentHighlightCenter)
|
||||
|
||||
expect(doc.elementFromPoint(...contentHighlightCenter)).eq(els.content[0])
|
||||
expect(highlightedEl).eq(els.content[0])
|
||||
|
||||
expectToBeInside(dims.content, dims.padding, 'content to be inside padding')
|
||||
expectToBeInside(dims.padding, dims.border, 'padding to be inside border')
|
||||
if (sel) {
|
||||
// assert convering bounding-box of element
|
||||
// assert converting bounding-box of element
|
||||
expectToBeEqual(dims.border, cy.$$(sel)[0].getBoundingClientRect(), 'border-box to match selector bounding-box')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,10 +32,9 @@ export const clickCommandLog = (sel, type) => {
|
||||
}
|
||||
|
||||
reactCommandInstance.props.appState.isRunning = false
|
||||
const inner = $(commandLogEl).find('.command-wrapper-text')
|
||||
|
||||
$(commandLogEl).find('.command-wrapper')
|
||||
.click()
|
||||
.get(0).scrollIntoView()
|
||||
inner.get(0).click()
|
||||
|
||||
// make sure command was pinned, otherwise throw a better error message
|
||||
expect(cy.$$('.runnable-active .command-pin', top.document).length, 'command should be pinned').ok
|
||||
|
||||
@@ -2,12 +2,86 @@ import _ from 'lodash'
|
||||
|
||||
import { $Command } from '../../../cypress/command'
|
||||
import $errUtils from '../../../cypress/error_utils'
|
||||
import group from '../../logGroup'
|
||||
|
||||
export default (Commands, Cypress, cy, state) => {
|
||||
const withinFn = (subject, fn) => {
|
||||
// reference the next command after this
|
||||
// within. when that command runs we'll
|
||||
// know to remove withinSubject
|
||||
const next = state('current').get('next')
|
||||
|
||||
// backup the current withinSubject
|
||||
// this prevents a bug where we null out
|
||||
// withinSubject when there are nested .withins()
|
||||
// we want the inner within to restore the outer
|
||||
// once its done
|
||||
const prevWithinSubject = state('withinSubject')
|
||||
|
||||
state('withinSubject', subject)
|
||||
|
||||
// https://github.com/cypress-io/cypress/pull/8699
|
||||
// An internal command is inserted to create a divider between
|
||||
// commands inside within() callback and commands chained to it.
|
||||
const restoreCmdIndex = state('index') + 1
|
||||
|
||||
cy.queue.insert(restoreCmdIndex, $Command.create({
|
||||
args: [subject],
|
||||
name: 'within-restore',
|
||||
fn: (subject) => subject,
|
||||
}))
|
||||
|
||||
state('index', restoreCmdIndex)
|
||||
|
||||
fn.call(cy.state('ctx'), subject)
|
||||
|
||||
const cleanup = () => cy.removeListener('command:start', setWithinSubject)
|
||||
|
||||
// we need a mechanism to know when we should remove
|
||||
// our withinSubject so we dont accidentally keep it
|
||||
// around after the within callback is done executing
|
||||
// so when each command starts, check to see if this
|
||||
// is the command which references our 'next' and
|
||||
// if so, remove the within subject
|
||||
const setWithinSubject = (obj) => {
|
||||
if (obj !== next) {
|
||||
return
|
||||
}
|
||||
|
||||
// okay so what we're doing here is creating a property
|
||||
// which stores the 'next' command which will reset the
|
||||
// withinSubject. If two 'within' commands reference the
|
||||
// exact same 'next' command, then this prevents accidentally
|
||||
// resetting withinSubject more than once. If they point
|
||||
// to different 'next's then its okay
|
||||
if (next !== state('nextWithinSubject')) {
|
||||
state('withinSubject', prevWithinSubject || null)
|
||||
state('nextWithinSubject', next)
|
||||
}
|
||||
|
||||
// regardless nuke this listeners
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// if next is defined then we know we'll eventually
|
||||
// unbind these listeners
|
||||
if (next) {
|
||||
cy.on('command:start', setWithinSubject)
|
||||
} else {
|
||||
// remove our listener if we happen to reach the end
|
||||
// event which will finalize cleanup if there was no next obj
|
||||
cy.once('command:queue:before:end', () => {
|
||||
cleanup()
|
||||
state('withinSubject', null)
|
||||
})
|
||||
}
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
Commands.addAll({ prevSubject: ['element', 'document'] }, {
|
||||
within (subject, options, fn) {
|
||||
let userOptions = options
|
||||
const ctx = this
|
||||
|
||||
if (_.isUndefined(fn)) {
|
||||
fn = userOptions
|
||||
@@ -16,90 +90,20 @@ export default (Commands, Cypress, cy, state) => {
|
||||
|
||||
options = _.defaults({}, userOptions, { log: true })
|
||||
|
||||
if (options.log) {
|
||||
options._log = Cypress.log({
|
||||
$el: subject,
|
||||
message: '',
|
||||
timeout: options.timeout,
|
||||
})
|
||||
const groupOptions: Cypress.LogGroup.Config = {
|
||||
log: options.log,
|
||||
$el: subject,
|
||||
message: '',
|
||||
timeout: options.timeout,
|
||||
}
|
||||
|
||||
if (!_.isFunction(fn)) {
|
||||
$errUtils.throwErrByPath('within.invalid_argument', { onFail: options._log })
|
||||
}
|
||||
|
||||
// reference the next command after this
|
||||
// within. when that command runs we'll
|
||||
// know to remove withinSubject
|
||||
const next = state('current').get('next')
|
||||
|
||||
// backup the current withinSubject
|
||||
// this prevents a bug where we null out
|
||||
// withinSubject when there are nested .withins()
|
||||
// we want the inner within to restore the outer
|
||||
// once its done
|
||||
const prevWithinSubject = state('withinSubject')
|
||||
|
||||
state('withinSubject', subject)
|
||||
|
||||
// https://github.com/cypress-io/cypress/pull/8699
|
||||
// An internal command is inserted to create a divider between
|
||||
// commands inside within() callback and commands chained to it.
|
||||
const restoreCmdIndex = state('index') + 1
|
||||
|
||||
cy.queue.insert(restoreCmdIndex, $Command.create({
|
||||
args: [subject],
|
||||
name: 'within-restore',
|
||||
fn: (subject) => subject,
|
||||
}))
|
||||
|
||||
state('index', restoreCmdIndex)
|
||||
|
||||
fn.call(ctx, subject)
|
||||
|
||||
const cleanup = () => cy.removeListener('command:start', setWithinSubject)
|
||||
|
||||
// we need a mechanism to know when we should remove
|
||||
// our withinSubject so we dont accidentally keep it
|
||||
// around after the within callback is done executing
|
||||
// so when each command starts, check to see if this
|
||||
// is the command which references our 'next' and
|
||||
// if so, remove the within subject
|
||||
const setWithinSubject = (obj) => {
|
||||
if (obj !== next) {
|
||||
return
|
||||
return group(Cypress, groupOptions, (log) => {
|
||||
if (!_.isFunction(fn)) {
|
||||
$errUtils.throwErrByPath('within.invalid_argument', { onFail: log })
|
||||
}
|
||||
|
||||
// okay so what we're doing here is creating a property
|
||||
// which stores the 'next' command which will reset the
|
||||
// withinSubject. If two 'within' commands reference the
|
||||
// exact same 'next' command, then this prevents accidentally
|
||||
// resetting withinSubject more than once. If they point
|
||||
// to differnet 'next's then its okay
|
||||
if (next !== state('nextWithinSubject')) {
|
||||
state('withinSubject', prevWithinSubject || null)
|
||||
state('nextWithinSubject', next)
|
||||
}
|
||||
|
||||
// regardless nuke this listeners
|
||||
cleanup()
|
||||
}
|
||||
|
||||
// if next is defined then we know we'll eventually
|
||||
// unbind these listeners
|
||||
if (next) {
|
||||
cy.on('command:start', setWithinSubject)
|
||||
} else {
|
||||
// remove our listener if we happen to reach the end
|
||||
// event which will finalize cleanup if there was no next obj
|
||||
cy.once('command:queue:before:end', () => {
|
||||
cleanup()
|
||||
|
||||
state('withinSubject', null)
|
||||
})
|
||||
}
|
||||
|
||||
return subject
|
||||
return withinFn(subject, fn)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class $Command {
|
||||
const arg = args[i]
|
||||
|
||||
if (_.isObject(arg)) {
|
||||
// filter out any properties which arent primitives
|
||||
// filter out any properties which aren't primitives
|
||||
// to prevent accidental mutations
|
||||
const opts = _.omitBy(arg, _.isObject)
|
||||
|
||||
|
||||
@@ -2028,7 +2028,20 @@ export default {
|
||||
docsUrl: 'https://on.cypress.io/cross-origin-script-error',
|
||||
},
|
||||
error_in_hook (obj) {
|
||||
let msg = `Because this error occurred during a \`${obj.hookName}\` hook we are skipping `
|
||||
let msg
|
||||
|
||||
if (obj.unsupportedPlugin && obj.errMessage) {
|
||||
msg = `${stripIndent`\
|
||||
Cypress detected that the current version of \`${obj.unsupportedPlugin}\` is not supported. Update it to the latest version
|
||||
|
||||
The following error was caught:
|
||||
|
||||
> ${obj.errMessage}
|
||||
|
||||
Because this error occurred during a \`${obj.hookName}\` hook we are skipping` } `
|
||||
} else {
|
||||
msg = `Because this error occurred during a \`${obj.hookName}\` hook we are skipping `
|
||||
}
|
||||
|
||||
const t = obj.parentTitle
|
||||
|
||||
|
||||
@@ -568,6 +568,24 @@ const logError = (Cypress, handlerType, err, handled = false) => {
|
||||
})
|
||||
}
|
||||
|
||||
const getUnsupportedPlugin = (runnable) => {
|
||||
if (!(runnable.invocationDetails && runnable.invocationDetails.originalFile && runnable.err && runnable.err.message)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const pluginsErrors = {
|
||||
'@cypress/code-coverage': 'glob pattern string required',
|
||||
}
|
||||
|
||||
const unsupportedPluginFound = Object.keys(pluginsErrors).find((plugin) => runnable.invocationDetails.originalFile.includes(plugin))
|
||||
|
||||
if (unsupportedPluginFound && pluginsErrors[unsupportedPluginFound] && runnable.err.message.includes(pluginsErrors[unsupportedPluginFound])) {
|
||||
return unsupportedPluginFound
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default {
|
||||
stackWithReplacedProps,
|
||||
appendErrMsg,
|
||||
@@ -577,6 +595,7 @@ export default {
|
||||
enhanceStack,
|
||||
errByPath,
|
||||
errorFromUncaughtEvent,
|
||||
getUnsupportedPlugin,
|
||||
getUserInvocationStack,
|
||||
getUserInvocationStackFromError,
|
||||
isAssertionErr,
|
||||
|
||||
@@ -13,7 +13,7 @@ import $errUtils from './error_utils'
|
||||
const groupsOrTableRe = /^(groups|table)$/
|
||||
const parentOrChildRe = /parent|child|system/
|
||||
const SNAPSHOT_PROPS = 'id snapshots $el url coords highlightAttr scrollBy viewportWidth viewportHeight'.split(' ')
|
||||
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ')
|
||||
const DISPLAY_PROPS = 'id alias aliasType callCount displayName end err event functionName groupLevel hookId instrument isStubbed group message method name numElements showError numResponses referencesAlias renderProps state testId timeout type url visible wallClockStartedAt testCurrentRetry'.split(' ')
|
||||
const BLACKLIST_PROPS = 'snapshots'.split(' ')
|
||||
|
||||
let counter = 0
|
||||
@@ -210,6 +210,7 @@ const defaults = function (state: Cypress.State, config, obj) {
|
||||
|
||||
if (logGroupIds.length) {
|
||||
obj.group = _.last(logGroupIds)
|
||||
obj.groupLevel = logGroupIds.length
|
||||
}
|
||||
|
||||
if (obj.groupEnd) {
|
||||
|
||||
@@ -987,16 +987,23 @@ const _runnerListeners = (_runner, Cypress, _emissions, getTestById, getTest, se
|
||||
hookName = getHookName(runnable)
|
||||
const test = getTest() || getTestFromHookOrFindTest(runnable)
|
||||
|
||||
const unsupportedPlugin = $errUtils.getUnsupportedPlugin(runnable)
|
||||
|
||||
// append a friendly message to the error indicating
|
||||
// we're skipping the remaining tests in this suite
|
||||
err = $errUtils.appendErrMsg(
|
||||
err,
|
||||
$errUtils.errByPath('uncaught.error_in_hook', {
|
||||
parentTitle,
|
||||
hookName,
|
||||
retries: test._retries,
|
||||
}).message,
|
||||
)
|
||||
const errMessage = $errUtils.errByPath('uncaught.error_in_hook', {
|
||||
parentTitle,
|
||||
hookName,
|
||||
retries: test._retries,
|
||||
unsupportedPlugin,
|
||||
errMessage: err.message,
|
||||
}).message
|
||||
|
||||
if (unsupportedPlugin) {
|
||||
err = $errUtils.modifyErrMsg(err, errMessage, () => errMessage)
|
||||
} else {
|
||||
err = $errUtils.appendErrMsg(err, errMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// always set runnable err so we can tap into
|
||||
|
||||
@@ -36,12 +36,14 @@
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">Your <span style="color:#de73ff">configFile<span style="color:#e05561"> is invalid: <span style="color:#4ec4ff">/path/to/config.ts<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">The <span style="color:#e5e510">component.devServer()<span style="color:#e05561"> must be a function with the following signature:<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">The <span style="color:#e5e510">component.devServer<span style="color:#e05561"> must be an object with a supported <span style="color:#e5e510">framework<span style="color:#e05561"> and <span style="color:#e5e510">bundler<span style="color:#e05561">.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff">{<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> component: {<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> devServer (cypressDevServerConfig, devServerConfig) {<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> <span style="color:#4f5666">// start dev server here<span style="color:#4ec4ff"><span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> devServer: {<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> framework: 'create-react-app', <span style="color:#4f5666">// Your framework<span style="color:#4ec4ff"><span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> bundler: 'webpack' <span style="color:#4f5666">// Your dev server<span style="color:#4ec4ff"><span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> }<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff"> }<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4ec4ff">}<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
@@ -50,5 +52,5 @@
|
||||
<span style="color:#e05561"><span style="color:#de73ff">{}<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">Learn more: https://on.cypress.io/dev-server<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6"></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
@@ -1347,20 +1347,24 @@ export const AllCypressErrors = {
|
||||
https://on.cypress.io/migration-guide`
|
||||
},
|
||||
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION: (configFilePath: string, setupNodeEvents: any) => {
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID: (configFilePath: string, setupNodeEvents: any) => {
|
||||
const re = /.?(cypress\.config.*)/
|
||||
const configFile = configFilePath.match(re)?.[1] ?? `configFile`
|
||||
|
||||
const code = errPartial`
|
||||
{
|
||||
component: {
|
||||
devServer (cypressDevServerConfig, devServerConfig) {
|
||||
${fmt.comment(`// start dev server here`)
|
||||
devServer: {
|
||||
framework: 'create-react-app', ${fmt.comment('// Your framework')}
|
||||
bundler: 'webpack' ${fmt.comment('// Your dev server')}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
return errTemplate`\
|
||||
Your ${fmt.highlightSecondary(`configFile`)} is invalid: ${fmt.path(configFilePath)}
|
||||
Your ${fmt.highlightSecondary(configFile)} is invalid: ${fmt.path(configFilePath)}
|
||||
|
||||
The ${fmt.highlight(`component.devServer()`)} must be a function with the following signature:
|
||||
The ${fmt.highlight(`component.devServer`)} must be an object with a supported ${fmt.highlight('framework')} and ${fmt.highlight('bundler')}.
|
||||
|
||||
${fmt.code(code)}
|
||||
|
||||
|
||||
@@ -1107,7 +1107,7 @@ describe('visual error templates', () => {
|
||||
default: [{ name: 'indexHtmlFile', configFile: '/path/to/cypress.config.js.ts' }],
|
||||
}
|
||||
},
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION: () => {
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID: () => {
|
||||
return {
|
||||
default: ['/path/to/config.ts', {}],
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export const createTestCurrentProject = (title: string, currentProject: Partial<
|
||||
...globalProject,
|
||||
__typename: 'CurrentProject',
|
||||
isCTConfigured: true,
|
||||
serveConfig: {},
|
||||
isE2EConfigured: true,
|
||||
currentTestingType: 'e2e',
|
||||
projectId: `${globalProject.title}-id`,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7035 2.46811C9.906 2.17768 9.00651 2 7.99995 2C5.12798 2 3.12771 3.44654 1.88166 4.83104C1.25934 5.52251 0.812476 6.21059 0.520557 6.72574C0.374034 6.98431 0.264985 7.20208 0.191132 7.35874C0.154168 7.43715 0.125908 7.50048 0.10608 7.54624C0.0961628 7.56913 0.0883443 7.58764 0.082591 7.60146L0.0755017 7.61866L0.0731082 7.62455L0.0721977 7.62681L0.0718138 7.62777C-0.0235506 7.86618 -0.023887 8.13298 0.0714774 8.37139L0.0721977 8.37319L0.0731082 8.37545L0.0755017 8.38134L0.082591 8.39854C0.0883443 8.41236 0.0961628 8.43087 0.10608 8.45376C0.125908 8.49952 0.154168 8.56285 0.191132 8.64126C0.264985 8.79792 0.374034 9.01569 0.520557 9.27426C0.812476 9.78941 1.25934 10.4775 1.88166 11.169C1.90067 11.1901 1.91985 11.2112 1.93921 11.2324L3.35514 9.81643C2.85921 9.26272 2.49785 8.70691 2.2606 8.28824C2.20027 8.18177 2.14836 8.08487 2.10467 8C2.14836 7.91512 2.20027 7.81823 2.2606 7.71176C2.49993 7.28941 2.86557 6.72749 3.36825 6.16896C4.3722 5.05346 5.87193 4 7.99995 4C8.37864 4 8.73744 4.03336 9.07708 4.09449L10.7035 2.46811ZM8.167 5.00457C8.1117 5.00154 8.05601 5 7.99995 5C6.3431 5 4.99995 6.34315 4.99995 8C4.99995 8.05605 5.00149 8.11175 5.00453 8.16705L8.167 5.00457ZM7.83299 10.9954L10.9954 7.83304C10.9984 7.88831 11 7.94397 11 8C11 9.65685 9.65681 11 7.99995 11C7.94393 11 7.88826 10.9985 7.83299 10.9954ZM6.92291 11.9055C7.26253 11.9666 7.62129 12 7.99995 12C10.128 12 11.6277 10.9465 12.6317 9.83104C13.1343 9.27251 13.5 8.71059 13.7393 8.28824C13.7996 8.18177 13.8515 8.08487 13.8952 8C13.8515 7.91512 13.7996 7.81823 13.7393 7.71176C13.5021 7.29311 13.1407 6.73731 12.6448 6.18361L14.0607 4.76769C14.0801 4.78881 14.0993 4.80993 14.1182 4.83104C14.7406 5.52251 15.1874 6.21059 15.4794 6.72574C15.6259 6.98431 15.7349 7.20208 15.8088 7.35874C15.8457 7.43715 15.874 7.50048 15.8938 7.54624C15.9037 7.56913 15.9116 7.58764 15.9173 7.60146L15.9244 7.61866L15.9268 7.62455L15.9281 7.62777L15 8L15.9281 8.37223L15.9268 8.37545L15.9244 8.38134L15.9173 8.39854C15.9116 8.41236 15.9037 8.43087 15.8938 8.45376C15.874 8.49952 15.8457 8.56285 15.8088 8.64126C15.7349 8.79792 15.6259 9.01569 15.4794 9.27426C15.1874 9.78941 14.7406 10.4775 14.1182 11.169C12.8722 12.5535 10.8719 14 7.99995 14C6.99342 14 6.09396 13.8223 5.29651 13.5319L6.92291 11.9055ZM15.9281 8.37223C15.9283 8.3718 15.9284 8.37139 15 8C15.9284 7.62861 15.9283 7.6282 15.9281 7.62777C16.0235 7.86618 16.0235 8.13382 15.9281 8.37223ZM0.0714774 8.37139L0.071474 8.37138C0.0713594 8.37095 0.0783897 8.36863 0.999042 8.00036L0.0714774 8.37139ZM0.999954 8L0.0718138 7.62777C0.0716417 7.6282 0.0714774 7.62861 0.999954 8Z" fill="currentColor" class="icon-dark"/>
|
||||
<path d="M2.5 13.5L13.5 2.5" stroke="currentColor" class="icon-dark" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 13.5L13.5 2.5" stroke="currentcolor" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -26,6 +26,8 @@
|
||||
:prefix-icon-class="open ? prefix?.classes + ' rotate-180' : prefix?.classes"
|
||||
:suffix-icon-aria-label="props.dismissible ? t('components.alert.dismissAriaLabel') : ''"
|
||||
:suffix-icon="props.dismissible ? DeleteIcon : null"
|
||||
:suffix-button-class="classes.suffixButtonClass"
|
||||
:suffix-icon-class="classes.suffixIconClass"
|
||||
data-cy="alert"
|
||||
class="rounded min-w-200px p-16px"
|
||||
@suffixIconClicked="$emit('update:modelValue', !modelValue)"
|
||||
|
||||
@@ -125,6 +125,14 @@ describe('<HeaderBarContent />', { viewportWidth: 1000, viewportHeight: 750 }, (
|
||||
})
|
||||
|
||||
it('shows hint and modal to upgrade to latest version of cypress', () => {
|
||||
// Set the clock to ensure that our percy snapshots always have the same relative time frame
|
||||
//
|
||||
// With this value they are:
|
||||
//
|
||||
// 8.7.0 - Released 7 months ago
|
||||
// 8.6.0 - Released last year
|
||||
cy.clock(Date.UTC(2022, 4, 26), ['Date'])
|
||||
|
||||
cy.mountFragment(HeaderBar_HeaderBarContentFragmentDoc, {
|
||||
onResult: (result) => {
|
||||
if (result.currentProject) {
|
||||
|
||||
@@ -66,7 +66,7 @@ export async function makeUrqlClient (config: UrqlClientConfig): Promise<Client>
|
||||
|
||||
const exchanges: Exchange[] = [dedupExchange]
|
||||
|
||||
const io = window.ws ?? getPubSubSource(config)
|
||||
const io = getPubSubSource(config)
|
||||
|
||||
const connectPromise = new Promise<void>((resolve) => {
|
||||
io.once('connect', resolve)
|
||||
@@ -77,9 +77,6 @@ export async function makeUrqlClient (config: UrqlClientConfig): Promise<Client>
|
||||
// GraphQL and urql are not used in app + run mode, so we don't add the
|
||||
// pub sub exchange.
|
||||
if (config.target === 'launchpad' || config.target === 'app' && !cypressInRunMode) {
|
||||
// If we're in the launchpad, we connect to the known GraphQL Socket port,
|
||||
// otherwise we connect to the /__socket of the current domain, unless we've explicitly
|
||||
|
||||
exchanges.push(pubSubExchange(io))
|
||||
}
|
||||
|
||||
@@ -161,15 +158,18 @@ interface AppPubSubConfig {
|
||||
|
||||
type PubSubConfig = LaunchpadPubSubConfig | AppPubSubConfig
|
||||
|
||||
// We need a dedicated socket.io namespace, rather than re-use the one provided via the runner
|
||||
// at window.ws, because the event-manager calls .off() on its socket instance when Cypress
|
||||
// execution is stopped. We need to make sure the events here are long-lived.
|
||||
function getPubSubSource (config: PubSubConfig) {
|
||||
if (config.target === 'launchpad') {
|
||||
return client({
|
||||
return client('/data-context', {
|
||||
path: '/__launchpad/socket',
|
||||
transports: ['websocket'],
|
||||
})
|
||||
}
|
||||
|
||||
return client({
|
||||
return client('/data-context', {
|
||||
path: config.socketIoRoute,
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
@@ -31,7 +31,7 @@ export const pubSubExchange = (io: Socket): Exchange => {
|
||||
}
|
||||
|
||||
// Handles the refresh of the GraphQL operation
|
||||
io.on('graphql-refresh', (refreshOnly?: RefreshOnlyInfo) => {
|
||||
io.on('graphql-refetch', (refreshOnly?: RefreshOnlyInfo) => {
|
||||
if (refreshOnly?.operation) {
|
||||
const fieldHeader = `${refreshOnly.operation}.${refreshOnly.field}`
|
||||
const toRefresh = Array.from(watchedOperations.values()).find((o) => getOperationName(o.query) === refreshOnly.operation)
|
||||
|
||||
@@ -241,12 +241,6 @@
|
||||
"configFileLanguageLabel": "Cypress Config File",
|
||||
"detected": "(detected)"
|
||||
},
|
||||
"install": {
|
||||
"startButton": "Install",
|
||||
"waitForInstall": "Waiting for you to install the dependencies...",
|
||||
"installed": "installed",
|
||||
"pendingInstall": "pending installation"
|
||||
},
|
||||
"step": {
|
||||
"continue": "Continue",
|
||||
"next": "Next Step",
|
||||
@@ -606,7 +600,12 @@
|
||||
},
|
||||
"installDependencies": {
|
||||
"title": "Install Dev Dependencies",
|
||||
"description": "Paste the command below into your terminal to install the required packages."
|
||||
"description": "Based on your previous selection, the following dependencies are required.",
|
||||
"pasteCommand": "Paste this command into your terminal to install the following packages:",
|
||||
"waitForInstall": "Waiting for you to install the dependencies...",
|
||||
"installed": "installed",
|
||||
"pendingInstall": "pending installation",
|
||||
"installationAlertSuccess": "You've successfully installed all required dependencies."
|
||||
},
|
||||
"configFiles": {
|
||||
"title": "Configuration Files",
|
||||
|
||||
@@ -447,6 +447,9 @@ type CurrentProject implements Node & ProjectLike {
|
||||
"""Project saved state"""
|
||||
savedState: JSON
|
||||
|
||||
"""base64 encoded config used on the runner page"""
|
||||
serveConfig: JSON!
|
||||
|
||||
"""A list of specs for the currently open testing type of a project"""
|
||||
specs: [Spec!]!
|
||||
title: String!
|
||||
@@ -510,7 +513,7 @@ enum ErrorTypeEnum {
|
||||
CHROME_WEB_SECURITY_NOT_SUPPORTED
|
||||
COMPONENT_FOLDER_REMOVED
|
||||
CONFIG_FILES_LANGUAGE_CONFLICT
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_A_FUNCTION
|
||||
CONFIG_FILE_DEV_SERVER_IS_NOT_VALID
|
||||
CONFIG_FILE_INVALID_DEV_START_EVENT
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG
|
||||
CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT
|
||||
@@ -1268,6 +1271,9 @@ type Subscription {
|
||||
"""
|
||||
cloudViewerChange: Query
|
||||
|
||||
"""Issued when cypress.config.js is re-executed due to a change"""
|
||||
configChange: CurrentProject
|
||||
|
||||
"""Issued for internal development changes"""
|
||||
devChange: DevState
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AddressInfo, Socket } from 'net'
|
||||
import { DataContext, getCtx, globalPubSub, GraphQLRequestInfo } from '@packages/data-context'
|
||||
import pDefer from 'p-defer'
|
||||
import cors from 'cors'
|
||||
import { SocketIOServer } from '@packages/socket'
|
||||
import { SocketIONamespace, SocketIOServer } from '@packages/socket'
|
||||
import type { Server } from 'http'
|
||||
import { graphqlHTTP } from 'express-graphql'
|
||||
import serverDestroy from 'server-destroy'
|
||||
@@ -21,7 +21,7 @@ const debug = debugLib(`cypress-verbose:graphql:operation`)
|
||||
|
||||
const IS_DEVELOPMENT = process.env.CYPRESS_INTERNAL_ENV !== 'production'
|
||||
|
||||
let gqlSocketServer: SocketIOServer
|
||||
let gqlSocketServer: SocketIONamespace
|
||||
let gqlServer: Server
|
||||
|
||||
globalPubSub.on('reset:data-context', (ctx) => {
|
||||
@@ -108,11 +108,13 @@ export async function makeGraphQLServer () {
|
||||
|
||||
serverDestroy(srv)
|
||||
|
||||
gqlSocketServer = new SocketIOServer(srv, {
|
||||
const socketSrv = new SocketIOServer(srv, {
|
||||
path: '/__launchpad/socket',
|
||||
transports: ['websocket'],
|
||||
})
|
||||
|
||||
gqlSocketServer = socketSrv.of('/data-context')
|
||||
|
||||
graphqlWS(srv, '/__launchpad/graphql-ws')
|
||||
|
||||
gqlSocketServer.on('connection', (socket) => {
|
||||
|
||||
@@ -102,7 +102,7 @@ export const CurrentProject = objectType({
|
||||
t.boolean('needsLegacyConfigMigration', {
|
||||
description: 'Whether the project needs to be migrated before proceeding',
|
||||
resolve (source, args, ctx) {
|
||||
return ctx.lifecycleManager.needsCypressJsonMigration()
|
||||
return ctx.migration.needsCypressJsonMigration()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -150,6 +150,13 @@ export const CurrentProject = objectType({
|
||||
},
|
||||
})
|
||||
|
||||
t.nonNull.json('serveConfig', {
|
||||
description: 'base64 encoded config used on the runner page',
|
||||
resolve: (source, args, ctx) => {
|
||||
return ctx.html.makeServeConfig()
|
||||
},
|
||||
})
|
||||
|
||||
t.json('savedState', {
|
||||
description: 'Project saved state',
|
||||
resolve: (source, args, ctx) => {
|
||||
|
||||
@@ -202,7 +202,7 @@ export const Migration = objectType({
|
||||
t.nonNull.string('configFileNameBefore', {
|
||||
description: 'the name of the config file to be migrated',
|
||||
resolve: (source, args, ctx) => {
|
||||
return ctx.lifecycleManager.legacyConfigFile
|
||||
return ctx.migration.legacyConfigFile
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -155,7 +155,7 @@ export const mutation = mutationType({
|
||||
|
||||
t.field('clearCurrentTestingType', {
|
||||
type: 'Query',
|
||||
resolve: async (_, args, ctx) => {
|
||||
resolve: (_, args, ctx) => {
|
||||
ctx.lifecycleManager.setAndLoadCurrentTestingType(null)
|
||||
|
||||
return {}
|
||||
|
||||
@@ -36,6 +36,13 @@ export const Subscription = subscriptionType({
|
||||
resolve: (source, args, ctx) => ctx.lifecycleManager,
|
||||
})
|
||||
|
||||
t.field('configChange', {
|
||||
type: CurrentProject,
|
||||
description: 'Issued when cypress.config.js is re-executed due to a change',
|
||||
subscribe: (source, args, ctx) => ctx.emitter.subscribeTo('configChange'),
|
||||
resolve: (source, args, ctx) => ctx.lifecycleManager,
|
||||
})
|
||||
|
||||
t.field('specsChange', {
|
||||
type: CurrentProject,
|
||||
description: 'Issued when the watched specs for the project changes',
|
||||
|
||||
@@ -199,6 +199,7 @@ describe('setupNodeEvents', () => {
|
||||
})
|
||||
|
||||
it('handles deprecated config fields in setupNodeEvents', () => {
|
||||
cy.scaffoldProject('pristine')
|
||||
cy.openProject('pristine')
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js',
|
||||
@@ -228,4 +229,35 @@ describe('setupNodeEvents', () => {
|
||||
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
})
|
||||
|
||||
it('handles multiple config errors and then recovers', () => {
|
||||
cy.scaffoldProject('pristine')
|
||||
cy.openProject('pristine')
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { baseUrl: 'htt://ocalhost:3000', e2e: { supportFile: false } }`)
|
||||
})
|
||||
|
||||
cy.openProject('pristine')
|
||||
|
||||
cy.visitLaunchpad()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
cy.get('[data-cy="alert-body"]').should('contain', 'Expected baseUrl to be a fully qualified URL')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { baseUrl: 'http://ocalhost:3000', e2e: { supportFile: false } }`)
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
cy.get('[data-cy-testingType=e2e]').click()
|
||||
cy.get('h1').should('contain', 'Error Loading Config')
|
||||
cy.get('[data-cy="alert-body"]').should('contain', 'The baseUrl configuration option is now invalid when set from the root of the config object')
|
||||
|
||||
cy.withCtx(async (ctx) => {
|
||||
await ctx.actions.file.writeFileInProject('cypress.config.js', `module.exports = { e2e: { baseUrl: 'http://localhost:3000', supportFile: false } }`)
|
||||
})
|
||||
|
||||
cy.findByRole('button', { name: 'Try again' }).click()
|
||||
cy.get('h1').should('contain', 'Choose a Browser')
|
||||
cy.get('[data-cy="alert"]').should('contain', 'Warning: Cannot Connect Base Url Warning')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ function migrateAndVerifyConfig (migratedConfigFile: string = 'cypress.config.js
|
||||
|
||||
expect(configStats).to.not.be.null.and.not.be.undefined
|
||||
|
||||
const oldConfigStats = await ctx.lifecycleManager.checkIfLegacyConfigFileExist()
|
||||
const oldConfigStats = ctx.migration.legacyConfigFileExists()
|
||||
|
||||
expect(oldConfigStats).to.be.false
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user