diff --git a/.vscode/settings.json b/.vscode/settings.json index 2824e4f981b2..5cee784b1cea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,5 +37,5 @@ // Volar is the main extension that powers Vue's language features. "volar.autoCompleteRefs": false, - "volar.takeOverMode.enabled": true + // "volar.takeOverMode.enabled": true } diff --git a/circle.yml b/circle.yml index 8bf15a5ef0bc..40c203177b67 100644 --- a/circle.yml +++ b/circle.yml @@ -39,6 +39,7 @@ macWorkflowFilters: &mac-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ '10.0-release', << pipeline.git.branch >> ] + - equal: [ 'tgriesser/10.0-release/refactor-lifecycle-ui', << pipeline.git.branch >> ] - equal: [ renovate/cypress-request-2.x, << pipeline.git.branch >> ] - matches: pattern: "-release$" @@ -49,6 +50,7 @@ windowsWorkflowFilters: &windows-workflow-filters or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ '10.0-release', << pipeline.git.branch >> ] + - equal: [ 'tgriesser/10.0-release/refactor-lifecycle-ui', << pipeline.git.branch >> ] - equal: [ test-binary-downstream-windows, << pipeline.git.branch >> ] - matches: pattern: "-release$" @@ -618,7 +620,6 @@ commands: command: | git fetch origin pull/<>/head:pr-<> git checkout pr-<> - git log -n 2 test-binary-against-rwa: description: | @@ -1334,7 +1335,7 @@ jobs: run-launchpad-integration-tests-chrome: <<: *defaults - parallelism: 1 + parallelism: 2 steps: - run-new-ui-tests: browser: chrome @@ -1344,7 +1345,7 @@ jobs: run-app-component-tests-chrome: <<: *defaults - parallelism: 1 + parallelism: 7 steps: - run-new-ui-tests: browser: chrome @@ -1354,7 +1355,7 @@ jobs: run-app-integration-tests-chrome: <<: *defaults - parallelism: 1 + parallelism: 2 steps: - run-new-ui-tests: browser: chrome @@ -1668,7 +1669,7 @@ jobs: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "test-binary-downstream-windows" && "$CIRCLE_BRANCH" != "10.0-release" && "$CIRCLE_BRANCH" != "renovate/cypress-request-2.x" && "$CIRCLE_BRANCH" != "tgriesser/fix/patch-resolutions" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "test-binary-downstream-windows" && "$CIRCLE_BRANCH" != "tgriesser/10.0-release/refactor-lifecycle-ui" && "$CIRCLE_BRANCH" != "10.0-release" && "$CIRCLE_BRANCH" != "renovate/cypress-request-2.x" && "$CIRCLE_BRANCH" != "tgriesser/fix/patch-resolutions" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 7ed3d85fe41f..b4ecf5de5297 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -288,6 +288,16 @@ const addCypressOpenCommand = (program) => { .option('--dev', text('dev'), coerceFalse) } +const maybeAddInspectFlags = (program) => { + if (process.argv.includes('--dev')) { + return program + .option('--inspect', 'Node option') + .option('--inspect-brk', 'Node option') + } + + return program +} + /** * Casts known command line options for "cypress run" to their intended type. * For example if the user passes "--port 5005" the ".port" property should be @@ -336,7 +346,7 @@ module.exports = { debug('creating program parser') const program = createProgram() - addCypressRunCommand(program) + maybeAddInspectFlags(addCypressRunCommand(program)) .action((...fnArgs) => { debug('parsed Cypress run %o', fnArgs) const options = parseVariableOpts(fnArgs, cliArgs) @@ -377,7 +387,7 @@ module.exports = { debug('creating program parser') const program = createProgram() - addCypressOpenCommand(program) + maybeAddInspectFlags(addCypressOpenCommand(program)) .action((...fnArgs) => { debug('parsed Cypress open %o', fnArgs) const options = parseVariableOpts(fnArgs, cliArgs) @@ -446,7 +456,7 @@ module.exports = { showVersions(args) }) - addCypressOpenCommand(program) + maybeAddInspectFlags(addCypressOpenCommand(program)) .action((opts) => { debug('opening Cypress') require('./exec/open') @@ -455,7 +465,7 @@ module.exports = { .catch(util.logErrorExit1) }) - addCypressRunCommand(program) + maybeAddInspectFlags(addCypressRunCommand(program)) .action((...fnArgs) => { debug('running Cypress with args %o', fnArgs) require('./exec/run') diff --git a/cli/lib/exec/open.js b/cli/lib/exec/open.js index 89fc406c994e..4708d276db4d 100644 --- a/cli/lib/exec/open.js +++ b/cli/lib/exec/open.js @@ -48,6 +48,14 @@ const processOpenOptions = (options = {}) => { args.push('--global', options.global) } + if (options.inspect) { + args.push('--inspect') + } + + if (options.inspectBrk) { + args.push('--inspectBrk') + } + args.push(...processTestingType(options)) debug('opening from options %j', options) diff --git a/cli/lib/exec/run.js b/cli/lib/exec/run.js index 6912728af44c..b8644b7a2921 100644 --- a/cli/lib/exec/run.js +++ b/cli/lib/exec/run.js @@ -137,6 +137,14 @@ const processRunOptions = (options = {}) => { args.push('--tag', options.tag) } + if (options.inspect) { + args.push('--inspect') + } + + if (options.inspectBrk) { + args.push('--inspectBrk') + } + args.push(...processTestingType(options)) return args diff --git a/cli/lib/util.js b/cli/lib/util.js index e0c537efe824..da089ee003f5 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -212,6 +212,8 @@ const parseOpts = (opts) => { 'group', 'headed', 'headless', + 'inspect', + 'inspectBrk', 'key', 'path', 'parallel', diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 58f6142699a0..7bf8d4008189 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2937,10 +2937,11 @@ declare namespace Cypress { */ type CoreConfigOptions = Partial> + type DevServerFn = (cypressConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise interface ComponentConfigOptions extends CoreConfigOptions { // TODO(tim): Keeping optional until we land the implementation - devServer?: (cypressConfig: DevServerConfig, devServerConfig: ComponentDevServerOpts) => ResolvedDevServerConfig | Promise - devServerConfig?: ComponentDevServerOpts + devServer?: Promise<{ devServer: DevServerFn}> | { devServer: DevServerFn } | DevServerFn + devServerConfig?: ComponentDevServerOpts | Promise } /** diff --git a/guides/app-lifecycle.md b/guides/app-lifecycle.md new file mode 100644 index 000000000000..5ae11b879584 --- /dev/null +++ b/guides/app-lifecycle.md @@ -0,0 +1,80 @@ +## App Lifecycle + +This documents the lifecycle of the application, specifically related to managing the current project, +and the various states & inputs that can feed into state changes, and how they are managed + +1. Application starts via `cypress open | run --flags` + 1. The input is run through `cli/lib/cli.js` for normalization + 1. The normalized input is passed into the server, eventually getting to `server/lib/modes/index.ts` +1. The `DataContext` class receives the testing mode (`run` | `open`), and the `modeOptions` (CLI Flags) +1. We call `ctx.initialize`, which based on the `mode` returns a promise for series of steps needed + 1. The `DataContext` should act as the global source of truth for all state in the application. It should be passed along where possible. In the `server` package, we can import/use `getCtx` so we don't need to pass it down the chain. + 1. The CLI flags & environment variables are used set the initial state of the `coreData` + 1. TODO: rename to `appState`? + 1. In `open` mode, if the `--global` flag is passed, we start in "global" mode, which allows us to select multiple projects + 1. Once a project is selected, either via the CLI being run within a project, or via the `--project` flag, we launch into project mode + +## Project Lifecycle + +1. Once a project is selected, we source the config from `cypress.config.js`, or wherever the config is specified via the `--configFile` CLI flag: + 1. Read the `globalBrowsers` + 1. Execute the `configFile` in a child process & reply back with the config, and the require.cache files in the child process + 1. If there is an error sourcing the config file, we set an error on the `currentProject` in the root state + 1. We source `cypress.env.json` and validate (if it exists) + +## **Config Precedence:** + +1. Runtime, inline: `it('should do the thing', { retries: { run: 3 } }` +2. `port` from spawned server +3. Returned from `setupNodeEvents` (as these get the options from the CLI) +4. Sourced from CLI +5. Sourced from `cypress.env.json` +6. Sourced from `cypress.config.{js|ts}` +7. Default config options + +## **Merging** + +Config options are deeply merged: + +```bash +# CLI: +cypress run --env FOO=bar + +# cypress.config.js +env: { + FOO: 'test' +}, +e2e: { + setupNodeEvents (on, config) { + return require('@cypress/code-coverage')(on, config) + }, + env: { + e2eRunner: true + } +} + +# Would Result in + +{ + env: { FOO: 'bar', e2eRunner: true } +} +``` + +## Steps of Sourcing / Execution + +1. **Application Start** + 1. CLI args & environment are parsed into an "options" object, which is passed along to create the initial application config + 2. Browsers are sourced from the machine at startup + 3. CLI options `--config baseUrl=http://example.com`, `--env` are gathered for merging later + 1. [https://gist.github.com/tgriesser/5111edc0e31b9db61755b0bddbf93e78](https://gist.github.com/tgriesser/5111edc0e31b9db61755b0bddbf93e78) +2. **Project Initialization** + 1. When we have a "projectRoot", we execute the `cypress.config.{js|ts}`, and read the `cypress.env.json` - this will be persisted on the state object, so we can compare the diff as we detect/watch changes to these files + 1. The child process will also send back a list of files that have been sourced so we can watch them for changes to re-execute the config. *We may want to warn against importing things top-level, so as to minimize the work done in child-process blocking the config* + 2. We also pull the "saved state" for the user from the FS App data + 1. We only do this in "open mode" + 3. At this point, we do a first-pass at creating a known config shape, merging the info together into a single object, picking out the "allowed" list of properties to pass to the `setupNodeEvents` +3. **setupNodeEvents** + 1. Once we have selected a `testingType`, we execute the `setupNodeEvents`, passing an "allowed" list of options as the second argument to the function. At this point, we have merged in any CLI options, env vars, + 1. If they return a new options object, we merge it with the one we passed in +4. **config → FullConfig** + 1. At this point we have the entire config, and we can set the `resolved` property which includes the origin of where the config property was resolved from diff --git a/npm/angular/cypress.config.ts b/npm/angular/cypress.config.ts index 69e14d50a43d..f87b7fd9362b 100644 --- a/npm/angular/cypress.config.ts +++ b/npm/angular/cypress.config.ts @@ -9,7 +9,9 @@ export default defineConfig({ 'component': { 'componentFolder': 'src/app', 'testFiles': '**/*cy-spec.ts', - 'setupNodeEvents': require('./cypress/plugins'), + setupNodeEvents (on, config) { + return require('./cypress/plugins')(on, config) + }, devServer (cypressConfig) { const { startDevServer } = require('@cypress/webpack-dev-server') const webpackConfig = require('./cypress/plugins/webpack.config') diff --git a/npm/react/plugins/utils/get-transpile-folders.js b/npm/react/plugins/utils/get-transpile-folders.js index b0b50dbec5ae..11cc92ccd76f 100644 --- a/npm/react/plugins/utils/get-transpile-folders.js +++ b/npm/react/plugins/utils/get-transpile-folders.js @@ -5,17 +5,27 @@ function getTranspileFolders (config) { const rawFolders = config.addTranspiledFolders ?? [] const folders = rawFolders.map((folder) => path.resolve(config.projectRoot, folder)) + // ensure path is absolute + // this is going away soon when we drop component and integration folder + const ensureAbs = (folder) => { + if (!path.isAbsolute(folder)) { + return path.resolve(folder) + } + + return folder + } + // user can disable folders, so check first if (config.componentFolder) { - folders.push(config.componentFolder) + folders.push(ensureAbs(config.componentFolder)) } if (config.fixturesFolder) { - folders.push(config.fixturesFolder) + folders.push(ensureAbs(config.fixturesFolder)) } if (config.supportFolder) { - folders.push(config.supportFolder) + folders.push(ensureAbs(config.supportFolder)) } return folders diff --git a/package.json b/package.json index 52008248b1e9..68c1b86f7e52 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "binary-release": "node ./scripts/binary.js release", "binary-upload": "node ./scripts/binary.js upload", "binary-zip": "node ./scripts/binary.js zip", - "build": "lerna run build --stream --no-bail --ignore create-cypress-tests && lerna run build --stream --scope create-cypress-tests", + "build": "lerna run build --stream --no-bail --ignore create-cypress-tests --ignore \"'@packages/{runner}'\" && lerna run build --stream --scope create-cypress-tests", "build-prod": "lerna run build-prod-ui --stream && lerna run build-prod --stream --ignore create-cypress-tests && lerna run build-prod --stream --scope create-cypress-tests", "bump": "node ./scripts/binary.js bump", "check-node-version": "node scripts/check-node-version.js", diff --git a/packages/app/cypress/e2e/integration/code-gen.spec.ts b/packages/app/cypress/e2e/integration/code-gen.spec.ts index 92974bdbd29c..8729e05fa53a 100644 --- a/packages/app/cypress/e2e/integration/code-gen.spec.ts +++ b/packages/app/cypress/e2e/integration/code-gen.spec.ts @@ -41,7 +41,7 @@ describe('Code Generation', () => { cy.findByTestId('file-row').contains('src/stories/Button.cy.js').click() cy.withCtx(async (ctx) => { - const spec = await (await ctx.project.findSpecs(ctx.currentProject?.projectRoot ?? '', 'component')) + const spec = (await ctx.project.findSpecs(ctx.currentProject ?? '', 'component')) .find((spec) => spec.relative === 'src/stories/Button.cy.jsx') expect(spec).to.exist @@ -62,7 +62,7 @@ describe('Code Generation', () => { cy.contains('composeStories') cy.withCtx(async (ctx) => { - const spec = await (await ctx.project.findSpecs(ctx.currentProject?.projectRoot ?? '', 'component')) + const spec = (await ctx.project.findSpecs(ctx.currentProject ?? '', 'component')) .find((spec) => spec.relative === 'src/stories/Button.stories.cy.jsx') expect(spec).to.exist diff --git a/packages/app/cypress/e2e/integration/runs.spec.ts b/packages/app/cypress/e2e/integration/runs.spec.ts index a7ad60bbe007..a3ed998f4425 100644 --- a/packages/app/cypress/e2e/integration/runs.spec.ts +++ b/packages/app/cypress/e2e/integration/runs.spec.ts @@ -56,12 +56,6 @@ describe('App: Runs Page', () => { it('when no project Id in the config file, shows call to action', () => { cy.withCtx(async (ctx) => { - if (ctx.currentProject) { - ctx.currentProject.configChildProcess?.process.kill() - ctx.currentProject.config = null - ctx.currentProject.configChildProcess = null - } - await ctx.actions.file.writeFileInProject('cypress.config.js', 'module.exports = {}') }) diff --git a/packages/app/cypress/e2e/integration/sidebar_navigation.spec.ts b/packages/app/cypress/e2e/integration/sidebar_navigation.spec.ts index a0684940f957..7c16560ed37b 100644 --- a/packages/app/cypress/e2e/integration/sidebar_navigation.spec.ts +++ b/packages/app/cypress/e2e/integration/sidebar_navigation.spec.ts @@ -1,5 +1,5 @@ describe('Sidebar Navigation', () => { - before(() => { + beforeEach(() => { cy.scaffoldProject('todos') cy.openProject('todos') cy.startAppServer() @@ -18,17 +18,17 @@ describe('Sidebar Navigation', () => { it('closes the bar when clicking the expand button (if expanded)', () => { cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'true') - cy.findByText('todos').should('be.visible') + cy.findByText('todos').as('title') + cy.get('@title').should('be.visible') cy.findByLabelText('toggle navigation', { selector: 'button', }).click() cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'false') - cy.findByText('todos').should('not.be.visible') + cy.get('@title').should('not.be.visible') }) it('has unlabeled menu item that shows the keyboard shortcuts modal (unexpanded state)', () => { - cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'false') cy.get('[data-cy="keyboard-shortcuts"]').should('be.visible') cy.get('[data-cy="keyboard-shortcuts"]').click() cy.get('h2').findByText('Keyboard Shortcuts').should('be.visible') @@ -43,7 +43,9 @@ describe('Sidebar Navigation', () => { }) it('shows a tooltip when hovering over menu item', () => { - cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'false') + cy.findByLabelText('toggle navigation', { + selector: 'button', + }).click() cy.get('[data-cy="sidebar-header"').realHover() cy.contains('#tooltip-target > div', 'todos').should('be.visible') @@ -67,7 +69,10 @@ describe('Sidebar Navigation', () => { }) it('opens the bar when clicking the expand button (if unexpanded)', () => { - cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'false') + cy.findByLabelText('toggle navigation', { + selector: 'button', + }).click() + cy.findByText('todos').should('not.be.visible') cy.findByLabelText('toggle navigation', { @@ -142,7 +147,7 @@ describe('Sidebar Navigation', () => { it('has a menu item labeled "Specs" which takes you to the Spec List page', () => { cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'true') - cy.get('[data-cy="app-header-bar"]').findByText('Specs-Index').should('not.exist') + // cy.get('[data-cy="app-header-bar"]').findByText('Specs-Index').should('not.exist') cy.findByText('Specs').should('be.visible') cy.findByText('Specs').click() cy.get('[data-cy="app-header-bar"]').findByText('Specs-Index').should('be.visible') diff --git a/packages/app/src/runner/SpecRunnerHeader.vue b/packages/app/src/runner/SpecRunnerHeader.vue index 7c098af99f9b..ae6c886b1998 100644 --- a/packages/app/src/runner/SpecRunnerHeader.vue +++ b/packages/app/src/runner/SpecRunnerHeader.vue @@ -102,7 +102,18 @@ fragment SpecRunnerHeader_Browser on Browser { gql` mutation SpecRunnerHeader_SetBrowser($browserId: ID!, $specPath: String!) { - launchpadSetBrowser(id: $browserId) + launchpadSetBrowser(id: $browserId) { + id + currentBrowser { + id + displayName + majorVersion + } + browsers { + id + isSelected + } + } launchOpenProject(specPath: $specPath) } ` diff --git a/packages/app/src/runs/RunsEmpty.vue b/packages/app/src/runs/RunsEmpty.vue index d52228a49a2a..af3cb4778ad0 100644 --- a/packages/app/src/runs/RunsEmpty.vue +++ b/packages/app/src/runs/RunsEmpty.vue @@ -14,7 +14,7 @@ scope="global" keypath="runs.empty.step1" > - {{ configFilePath }} + {{ configFile }}

- {{ configFilePath }} + {{ configFile }}

{ }) const projectName = computed(() => props.gql.title) -const configFilePath = computed(() => props.gql.configFilePath) +const configFile = computed(() => props.gql.configFile) const firstRecordKey = computed(() => { return props.gql.cloudProject?.__typename === 'CloudProject' && props.gql.cloudProject.recordKeys?.[0] ? props.gql.cloudProject?.recordKeys?.[0]?.key diff --git a/packages/app/src/settings/project/Config.vue b/packages/app/src/settings/project/Config.vue index e0dba396cff5..906ff1421bdf 100644 --- a/packages/app/src/settings/project/Config.vue +++ b/packages/app/src/settings/project/Config.vue @@ -11,7 +11,7 @@ scope="global" keypath="settingsPage.config.description" > - +
@@ -21,7 +21,6 @@ />
@@ -45,8 +44,6 @@ fragment Config on Query { id config } - ...ConfigLegend - ...OpenConfigFileInIDE } ` diff --git a/packages/app/src/settings/project/ConfigLegend.spec.tsx b/packages/app/src/settings/project/ConfigLegend.spec.tsx index 1e6e0a253581..abd644369ff2 100644 --- a/packages/app/src/settings/project/ConfigLegend.spec.tsx +++ b/packages/app/src/settings/project/ConfigLegend.spec.tsx @@ -1,17 +1,12 @@ import { defaultMessages } from '@cy/i18n' import ConfigLegend from './ConfigLegend.vue' import { each } from 'lodash' -import { ConfigLegendFragmentDoc } from '../../generated/graphql-test' const legend = defaultMessages.settingsPage.config.legend describe('', () => { it('renders', () => { - cy.mountFragment(ConfigLegendFragmentDoc, { - render (gqlVal) { - return - }, - }) + cy.mount(ConfigLegend) each(legend, ({ label, description }) => { cy.contains(label) diff --git a/packages/app/src/settings/project/ConfigLegend.vue b/packages/app/src/settings/project/ConfigLegend.vue index 3b9acb5a8ef2..a3789e5e2f65 100644 --- a/packages/app/src/settings/project/ConfigLegend.vue +++ b/packages/app/src/settings/project/ConfigLegend.vue @@ -15,7 +15,7 @@ scope="global" :keypath="legendText.config.descriptionKey" > - + @@ -45,7 +45,7 @@ href="https://on.cypress.io" class="text-purple-500" > - setupNodeEnv + setupNodeEvents @@ -58,19 +58,7 @@ import ExternalLink from '@cy/gql-components/ExternalLink.vue' import { computed } from 'vue' import { useI18n } from '@cy/i18n' import { CONFIG_LEGEND_COLOR_MAP } from './ConfigSourceColors' -import type { ConfigLegendFragment } from '../../generated/graphql' import OpenConfigFileInIDE from '@packages/frontend-shared/src/gql-components/OpenConfigFileInIDE.vue' -import { gql } from '@urql/vue' - -gql` -fragment ConfigLegend on Query { - ...OpenConfigFileInIDE -} -` - -const props = defineProps<{ - gql: ConfigLegendFragment -}>() const { t } = useI18n() const legendText = computed(() => { diff --git a/packages/app/src/specs/CustomPatternNoSpecContent.vue b/packages/app/src/specs/CustomPatternNoSpecContent.vue index 6b3be2f36058..aed75a94cbec 100644 --- a/packages/app/src/specs/CustomPatternNoSpecContent.vue +++ b/packages/app/src/specs/CustomPatternNoSpecContent.vue @@ -3,7 +3,7 @@
- +