diff --git a/circle.yml b/circle.yml index 6d8adda01e65..6b37240fd9a6 100644 --- a/circle.yml +++ b/circle.yml @@ -27,8 +27,6 @@ mainBuildFilters: &mainBuildFilters branches: only: - develop - - reapply-state-refactor - - fix-or-skip-flaky-tests # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -46,6 +44,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] + - equal: [ "lmiller/experimental-single-tab-component-testing", << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 5800ab9c83bf..8c6a20e487af 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3062,6 +3062,11 @@ declare namespace Cypress { interface ComponentConfigOptions extends Omit { devServer: DevServerFn | DevServerConfigOptions devServerConfig?: ComponentDevServerOpts + /** + * Runs all component specs in a single tab, trading spec isolation for faster run mode execution. + * @default false + */ + experimentalSingleTabRunMode?: boolean } /** diff --git a/packages/app/cypress/component/support/ctSupport.ts b/packages/app/cypress/component/support/ctSupport.ts index bf8561b447b2..8cc2a4393a75 100644 --- a/packages/app/cypress/component/support/ctSupport.ts +++ b/packages/app/cypress/component/support/ctSupport.ts @@ -10,6 +10,17 @@ export const StubWebsocket = new Proxy(Object.create(null), { }, }) +beforeEach(() => { + if (!window.top?.getEventManager) { + throw Error('Could not find `window.top.getEventManager`. Expected `getEventManager` to be defined.') + } + + // this is always undefined, since we only define it when + // running CT with a project that sets `experimentalSingleTabRunMode: true` + // @ts-ignore - dynamically defined during tests using + expect(window.top.getEventManager().autDestroyedCount).to.be.undefined +}) + // Event manager with Cypress driver dependencies stubbed out // Useful for component testing export const createEventManager = () => { diff --git a/packages/app/cypress/e2e/support/e2eSupport.ts b/packages/app/cypress/e2e/support/e2eSupport.ts index c0a47eb9af1f..f3f41da83858 100644 --- a/packages/app/cypress/e2e/support/e2eSupport.ts +++ b/packages/app/cypress/e2e/support/e2eSupport.ts @@ -1,3 +1,10 @@ import '@packages/frontend-shared/cypress/e2e/support/e2eSupport' import 'cypress-real-events/support' import './execute-spec' + +beforeEach(() => { + // this is always 0, since we only destroy the AUT when using + // `experimentalSingleTabRunMode, which is not a valid experiment for for e2e testing. + // @ts-ignore - dynamically defined during tests using + expect(window.top?.getEventManager().autDestroyedCount).to.be.undefined +}) diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 23c8ad3098b1..d54d4f6bf13d 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -35,6 +35,14 @@ export class AutIframe { return $iframe } + destroy () { + if (!this.$iframe) { + throw Error(`Cannot call #remove without first calling #create`) + } + + this.$iframe.remove() + } + showInitialBlankContents () { this._showContents(blankContents.initial()) } diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index 54c74bfe4c13..4f7238068b8e 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -11,6 +11,7 @@ import type { Socket } from '@packages/socket/lib/browser' import * as cors from '@packages/network/lib/cors' import { automation, useRunnerUiStore } from '../store' import { useScreenshotStore } from '../store/screenshot-store' +import { getAutIframeModel } from '.' export type CypressInCypressMochaEvent = Array>> @@ -54,6 +55,8 @@ export class EventManager { studioRecorder: any selectorPlaygroundModel: any cypressInCypressMochaEvents: CypressInCypressMochaEvent[] = [] + // Used for testing the experimentalSingleTabRunMode experiment. Ensures AUT is correctly destroyed between specs. + ws: Socket constructor ( // import '@packages/driver' @@ -64,10 +67,11 @@ export class EventManager { selectorPlaygroundModel: any, // StudioRecorder constructor StudioRecorderCtor: any, - private ws: Socket, + ws: Socket, ) { this.studioRecorder = new StudioRecorderCtor(this) this.selectorPlaygroundModel = selectorPlaygroundModel + this.ws = ws } getCypress () { @@ -337,6 +341,13 @@ export class EventManager { rerun() }) + this.ws.on('aut:destroy:init', () => { + const autIframe = getAutIframeModel() + + autIframe.destroy() + this.ws.emit('aut:destroy:complete') + }) + // @ts-ignore const $window = this.$CypressDriver.$(window) diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js index 61f61911bd9c..f8433dda93a2 100644 --- a/packages/config/__snapshots__/index.spec.ts.js +++ b/packages/config/__snapshots__/index.spec.ts.js @@ -39,6 +39,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1 "experimentalSessionAndOrigin": false, "experimentalModifyObstructiveThirdPartyCode": false, "experimentalSourceRewriting": false, + "experimentalSingleTabRunMode": false, "fileServerFolder": "", "fixturesFolder": "cypress/fixtures", "excludeSpecPattern": "*.hot-update.js", @@ -120,6 +121,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f "experimentalSessionAndOrigin": false, "experimentalModifyObstructiveThirdPartyCode": false, "experimentalSourceRewriting": false, + "experimentalSingleTabRunMode": false, "fileServerFolder": "", "fixturesFolder": "cypress/fixtures", "excludeSpecPattern": "*.hot-update.js", @@ -197,6 +199,7 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key "experimentalSessionAndOrigin", "experimentalModifyObstructiveThirdPartyCode", "experimentalSourceRewriting", + "experimentalSingleTabRunMode", "fileServerFolder", "fixturesFolder", "excludeSpecPattern", diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index eec8fab1ffa8..9a67529c084c 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -75,7 +75,7 @@ export function resetIssuedWarnings () { issuedWarnings.clear() } -const validateNoBreakingOptions = (breakingCfgOptions: BreakingOption[], cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType?: TestingType) => { +const validateNoBreakingOptions = (breakingCfgOptions: Readonly, cfg: any, onWarning: ErrorHandler, onErr: ErrorHandler, testingType?: TestingType) => { breakingCfgOptions.forEach(({ name, errorKey, newName, isWarning, value }) => { if (_.has(cfg, name)) { if (value && cfg[name] !== value) { diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index 7bb3d9549a2b..31b629ea08cb 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -3,31 +3,37 @@ import path from 'path' // @ts-ignore import pkg from '@packages/root' +import type { AllCypressErrorNames } from '@packages/errors' import type { TestingType } from '@packages/types' import * as validate from './validation' -export type BreakingOptionErrorKey = - | 'COMPONENT_FOLDER_REMOVED' - | 'INTEGRATION_FOLDER_REMOVED' - | 'CONFIG_FILE_INVALID_ROOT_CONFIG' - | 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E' - | 'CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT' - | 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT' - | 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E' - | 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED' - | 'EXPERIMENTAL_SAMESITE_REMOVED' - | 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED' - | 'EXPERIMENTAL_RUN_EVENTS_REMOVED' - | 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED' - | 'EXPERIMENTAL_SHADOW_DOM_REMOVED' - | 'EXPERIMENTAL_STUDIO_REMOVED' - | 'FIREFOX_GC_INTERVAL_REMOVED' - | 'NODE_VERSION_DEPRECATION_SYSTEM' - | 'NODE_VERSION_DEPRECATION_BUNDLED' - | 'PLUGINS_FILE_CONFIG_OPTION_REMOVED' - | 'RENAMED_CONFIG_OPTION' - | 'TEST_FILES_RENAMED' +const BREAKING_OPTION_ERROR_KEY: Readonly = [ + 'COMPONENT_FOLDER_REMOVED', + 'INTEGRATION_FOLDER_REMOVED', + 'CONFIG_FILE_INVALID_ROOT_CONFIG', + 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E', + 'CONFIG_FILE_INVALID_ROOT_CONFIG_COMPONENT', + 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT', + 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E', + 'EXPERIMENTAL_COMPONENT_TESTING_REMOVED', + 'EXPERIMENTAL_SAMESITE_REMOVED', + 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED', + 'EXPERIMENTAL_RUN_EVENTS_REMOVED', + 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED', + 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE', + 'EXPERIMENTAL_SHADOW_DOM_REMOVED', + 'EXPERIMENTAL_STUDIO_REMOVED', + 'EXPERIMENTAL_STUDIO_REMOVED', + 'FIREFOX_GC_INTERVAL_REMOVED', + 'NODE_VERSION_DEPRECATION_SYSTEM', + 'NODE_VERSION_DEPRECATION_BUNDLED', + 'PLUGINS_FILE_CONFIG_OPTION_REMOVED', + 'RENAMED_CONFIG_OPTION', + 'TEST_FILES_RENAMED', +] as const + +export type BreakingOptionErrorKey = typeof BREAKING_OPTION_ERROR_KEY[number] export type OverrideLevel = 'any' | 'suite' | 'never' @@ -211,6 +217,12 @@ const driverConfigOptions: Array = [ validation: validate.isBoolean, isExperimental: true, requireRestartOnChange: 'server', + }, { + name: 'experimentalSingleTabRunMode', + defaultValue: false, + validation: validate.isBoolean, + isExperimental: true, + requireRestartOnChange: 'server', }, { name: 'fileServerFolder', defaultValue: '', @@ -520,7 +532,7 @@ export const options: Array = [ /** * Values not allowed in 10.X+ in the root, e2e and component config */ -export const breakingOptions: Array = [ +export const breakingOptions: Readonly = [ { name: 'blacklistHosts', errorKey: 'RENAMED_CONFIG_OPTION', @@ -592,7 +604,7 @@ export const breakingOptions: Array = [ newName: 'specPattern', isWarning: false, }, -] +] as const export const breakingRootOptions: Array = [ { @@ -645,6 +657,12 @@ export const breakingRootOptions: Array = [ export const testingTypeBreakingOptions: { e2e: Array, component: Array } = { e2e: [ + { + name: 'experimentalSingleTabRunMode', + errorKey: 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE', + isWarning: false, + testingTypes: ['e2e'], + }, { name: 'indexHtmlFile', errorKey: 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E', diff --git a/packages/errors/__snapshot-html__/COMPONENT_TESTING_MISMATCHED_DEPENDENCIES.html b/packages/errors/__snapshot-html__/COMPONENT_TESTING_MISMATCHED_DEPENDENCIES.html index a7efe2148941..97afac306be8 100644 --- a/packages/errors/__snapshot-html__/COMPONENT_TESTING_MISMATCHED_DEPENDENCIES.html +++ b/packages/errors/__snapshot-html__/COMPONENT_TESTING_MISMATCHED_DEPENDENCIES.html @@ -36,7 +36,7 @@
We detected that you have versions of dependencies that are not officially supported:
 
- - `vite`. Expected >=2.0.0, found 1.0.0.
+ - `vite`. Expected ^=2.0.0 || ^=3.0.0, found 1.0.0.
 
 If you're experiencing problems, downgrade dependencies and restart Cypress.
 
diff --git a/packages/errors/__snapshot-html__/EXPERIMENTAL_SINGLE_TAB_RUN_MODE.html b/packages/errors/__snapshot-html__/EXPERIMENTAL_SINGLE_TAB_RUN_MODE.html
new file mode 100644
index 000000000000..406c55199f96
--- /dev/null
+++ b/packages/errors/__snapshot-html__/EXPERIMENTAL_SINGLE_TAB_RUN_MODE.html
@@ -0,0 +1,40 @@
+
+    
+    
+      
+      
+    
+    
+    
+    
+  
+    
+    
The experimentalSingleTabRunMode experiment is currently only supported for Component Testing.
+
+If you have feedback about the experiment, please join the discussion here: http://on.cypress.io/single-tab-run-mode
+
\ No newline at end of file diff --git a/packages/errors/src/errors.ts b/packages/errors/src/errors.ts index 7505811027ed..0ed3d43d4ea8 100644 --- a/packages/errors/src/errors.ts +++ b/packages/errors/src/errors.ts @@ -1085,6 +1085,12 @@ export const AllCypressErrors = { You can safely remove the ${fmt.highlight(`experimentalStudio`)} configuration option from your config.` }, + EXPERIMENTAL_SINGLE_TAB_RUN_MODE: () => { + return errTemplate`\ + The ${fmt.highlight(`experimentalSingleTabRunMode`)} experiment is currently only supported for Component Testing. + + If you have feedback about the experiment, please join the discussion here: http://on.cypress.io/single-tab-run-mode` + }, FIREFOX_GC_INTERVAL_REMOVED: () => { return errTemplate`\ The ${fmt.highlight(`firefoxGcInterval`)} configuration option was removed in ${fmt.cypressVersion(`8.0.0`)}. It was introduced to work around a bug in Firefox 79 and below. diff --git a/packages/errors/test/unit/visualSnapshotErrors_spec.ts b/packages/errors/test/unit/visualSnapshotErrors_spec.ts index 9bb86b7ace63..fb0e71e70963 100644 --- a/packages/errors/test/unit/visualSnapshotErrors_spec.ts +++ b/packages/errors/test/unit/visualSnapshotErrors_spec.ts @@ -1184,7 +1184,7 @@ describe('visual error templates', () => { package: 'vite', installer: 'vite', description: 'Vite is dev server that serves your source files over native ES modules', - minVersion: '>=2.0.0', + minVersion: '^=2.0.0 || ^=3.0.0', }, satisfied: false, detectedVersion: '1.0.0', @@ -1194,5 +1194,11 @@ describe('visual error templates', () => { ], } }, + + EXPERIMENTAL_SINGLE_TAB_RUN_MODE: () => { + return { + default: [], + } + }, }) }) diff --git a/packages/frontend-shared/src/gql-components/error/BaseError.cy.tsx b/packages/frontend-shared/src/gql-components/error/BaseError.cy.tsx index c1d334fbdad0..5d8e9b36a29c 100644 --- a/packages/frontend-shared/src/gql-components/error/BaseError.cy.tsx +++ b/packages/frontend-shared/src/gql-components/error/BaseError.cy.tsx @@ -4,7 +4,7 @@ import { BaseErrorFragmentDoc } from '../../../../launchpad/src/generated/graphq import dedent from 'dedent' // Selectors -const headerSelector = 'h1[data-testid=error-header]' +const headerSelector = 'h1[data-cy=error-header]' const messageSelector = '[data-testid=error-message]' const retryButtonSelector = 'button[data-testid=error-retry-button]' const docsButtonSelector = 'a[data-testid=error-docs-button]' diff --git a/packages/frontend-shared/src/gql-components/error/BaseError.vue b/packages/frontend-shared/src/gql-components/error/BaseError.vue index 45b534da7a94..e616a7a93332 100644 --- a/packages/frontend-shared/src/gql-components/error/BaseError.vue +++ b/packages/frontend-shared/src/gql-components/error/BaseError.vue @@ -8,7 +8,7 @@

{{ baseError.title }} diff --git a/packages/launchpad/cypress.config.ts b/packages/launchpad/cypress.config.ts index c52816ef08fa..35265d7ffe5a 100644 --- a/packages/launchpad/cypress.config.ts +++ b/packages/launchpad/cypress.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ videoCompression: false, // turn off video compression for CI }, component: { + experimentalSingleTabRunMode: true, supportFile: 'cypress/component/support/index.ts', devServer: { bundler: 'vite', diff --git a/packages/launchpad/cypress/e2e/config-warning.cy.ts b/packages/launchpad/cypress/e2e/config-warning.cy.ts index 50386fa3e184..222983059b69 100644 --- a/packages/launchpad/cypress/e2e/config-warning.cy.ts +++ b/packages/launchpad/cypress/e2e/config-warning.cy.ts @@ -67,6 +67,26 @@ describe('baseUrl', () => { }) }) +describe('experimentalSingleTabRunMode', () => { + it('is a valid config for component testing', () => { + cy.scaffoldProject('experimentalSingleTabRunMode') + cy.openProject('experimentalSingleTabRunMode') + cy.visitLaunchpad() + cy.get('[data-cy-testingtype="component"]').click() + cy.get('h1').contains('Initializing Config').should('not.exist') + cy.get('h1').contains('Choose a Browser') + }) + + it('is not a valid config for e2e testing', () => { + cy.scaffoldProject('experimentalSingleTabRunMode') + cy.openProject('experimentalSingleTabRunMode') + cy.visitLaunchpad() + cy.get('[data-cy-testingtype="e2e"]').click() + cy.findByTestId('error-header').contains('Cypress configuration error') + cy.findByTestId('alert-body').contains('The experimentalSingleTabRunMode experiment is currently only supported for Component Testing.') + }) +}) + describe('experimentalStudio', () => { it('should show experimentalStudio warning if Cypress detects experimentalStudio config has been set', () => { cy.scaffoldProject('experimental-studio') diff --git a/packages/server/lib/experiments.ts b/packages/server/lib/experiments.ts index cb09134281e3..9cf2bb57bf6d 100644 --- a/packages/server/lib/experiments.ts +++ b/packages/server/lib/experiments.ts @@ -56,6 +56,7 @@ const _summaries: StringValues = { experimentalSessionAndOrigin: 'Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands.', experimentalModifyObstructiveThirdPartyCode: 'Applies `modifyObstructiveCode` to third party `.html` and `.js`, removes subresource integrity, and modifies the user agent in Electron.', experimentalSourceRewriting: 'Enables AST-based JS/HTML rewriting. This may fix issues caused by the existing regex-based JS/HTML replacement algorithm.', + experimentalSingleTabRunMode: 'Runs all component specs in a single tab, trading spec isolation for faster run mode execution.', experimentalStudio: 'Generate and save commands directly to your test suite by interacting with your app as an end user would.', } @@ -74,6 +75,7 @@ const _names: StringValues = { experimentalInteractiveRunEvents: 'Interactive Mode Run Events', experimentalSessionAndOrigin: 'Cross-origin and Session', experimentalModifyObstructiveThirdPartyCode: 'Modify Obstructive Third Party Code', + experimentalSingleTabRunMode: 'Single Tab Run Mode', experimentalSourceRewriting: 'Improved Source Rewriting', experimentalStudio: 'Studio', } diff --git a/packages/server/lib/modes/run.js b/packages/server/lib/modes/run.js index 2a870d702bbf..79f38bf3b934 100644 --- a/packages/server/lib/modes/run.js +++ b/packages/server/lib/modes/run.js @@ -776,6 +776,12 @@ module.exports = { displayRunStarting, + navigateToNextSpec (spec) { + debug('navigating to next spec %s', spec) + + return openProject.changeUrlToSpec(spec) + }, + exitEarly (err) { debug('set early exit error: %s', err.stack) @@ -1105,6 +1111,16 @@ module.exports = { return this.currentSetScreenshotMetadata(data) } + if (options.experimentalSingleTabRunMode && options.testingType === 'component' && !options.isFirstSpec) { + // reset browser state to match default behavior when opening/closing a new tab + return openProject.resetBrowserState().then(() => { + // If we do not launch the browser, + // we tell it that we are ready + // to receive the next spec + return this.navigateToNextSpec(options.spec) + }) + } + const wait = () => { debug('waiting for socket to connect and browser to launch...') @@ -1168,7 +1184,7 @@ module.exports = { }, waitForTestsToFinishRunning (options = {}) { - const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet, config, shouldKeepTabOpen } = options + const { project, screenshots, startedVideoCapture, endVideoCapture, videoName, compressedVideoName, videoCompression, videoUploadOnPasses, exit, spec, estimated, quiet, config, shouldKeepTabOpen, testingType } = options // https://github.com/cypress-io/cypress/issues/2370 // delay 1 second if we're recording a video to give @@ -1244,17 +1260,22 @@ module.exports = { } } - // Close the browser if the environment variable is set to do so - // if (process.env.CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH) { - // debug('attempting to close the browser') - // await openProject.closeBrowser() - // } else { - debug('attempting to close the browser tab') - await openProject.resetBrowserTabsForNextTest(shouldKeepTabOpen) - // } + const usingExperimentalSingleTabMode = testingType === 'component' && config.experimentalSingleTabRunMode - debug('resetting server state') - openProject.projectBase.server.reset() + if (usingExperimentalSingleTabMode) { + await openProject.projectBase.server.destroyAut() + } + + // we do not support experimentalSingleTabRunMode for e2e + if (!usingExperimentalSingleTabMode) { + debug('attempting to close the browser tab') + + await openProject.resetBrowserTabsForNextTest(shouldKeepTabOpen) + + debug('resetting server state') + + openProject.projectBase.server.reset() + } if (videoExists && !skippedSpec && endVideoCapture && !videoCaptureFailed) { const ffmpegChaptersConfig = videoCapture.generateFfmpegChaptersConfig(results.tests) @@ -1479,6 +1500,7 @@ module.exports = { endVideoCapture: videoRecordProps.endVideoCapture, startedVideoCapture: videoRecordProps.startedVideoCapture, exit: options.exit, + testingType: options.testingType, videoCompression: options.videoCompression, videoUploadOnPasses: options.videoUploadOnPasses, quiet: options.quiet, @@ -1496,8 +1518,10 @@ module.exports = { socketId: options.socketId, webSecurity: options.webSecurity, projectRoot: options.projectRoot, + testingType: options.testingType, + isFirstSpec, + experimentalSingleTabRunMode: config.experimentalSingleTabRunMode, shouldLaunchNewTab: !isFirstSpec, // !process.env.CYPRESS_INTERNAL_FORCE_BROWSER_RELAUNCH && !isFirstSpec, - // TODO(tim): investigate the socket disconnect }), }) }) diff --git a/packages/server/lib/open_project.ts b/packages/server/lib/open_project.ts index d4f00ff2fd46..8c10e92b7c6b 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -223,6 +223,21 @@ export class OpenProject { this.closeOpenProjectAndBrowsers() } + changeUrlToSpec (spec: Cypress.Spec) { + if (!this.projectBase) { + return + } + + const newSpecUrl = getSpecUrl({ + projectRoot: this.projectBase.projectRoot, + spec, + }) + + debug(`New url is ${newSpecUrl}`) + + this.projectBase.server._socket.changeToUrl(newSpecUrl) + } + // close existing open project if it exists, for example // if you are switching from CT to E2E or vice versa. // used by launchpad diff --git a/packages/server/lib/server-ct.ts b/packages/server/lib/server-ct.ts index d5b6d7240ab1..5bc9a0be2dbf 100644 --- a/packages/server/lib/server-ct.ts +++ b/packages/server/lib/server-ct.ts @@ -48,4 +48,8 @@ export class ServerCt extends ServerBase { }) }) } + + destroyAut () { + return this.socket.destroyAut() + } } diff --git a/packages/server/lib/socket-base.ts b/packages/server/lib/socket-base.ts index b802379b09be..2b16a296f1e2 100644 --- a/packages/server/lib/socket-base.ts +++ b/packages/server/lib/socket-base.ts @@ -609,4 +609,8 @@ export class SocketBase { close () { return this._io?.close() } + + changeToUrl (url: string) { + return this.toRunner('change:to:url', url) + } } diff --git a/packages/server/lib/socket-ct.ts b/packages/server/lib/socket-ct.ts index 0d8a0d3584e8..2e91a42b0a22 100644 --- a/packages/server/lib/socket-ct.ts +++ b/packages/server/lib/socket-ct.ts @@ -1,12 +1,16 @@ import Debug from 'debug' -import type * as socketIo from '@packages/socket' import devServer from '@packages/server/lib/plugins/dev-server' import { SocketBase } from '@packages/server/lib/socket-base' +import dfd from 'p-defer' +import type { Socket } from '@packages/socket' import type { DestroyableHttpServer } from '@packages/server/lib/util/server_destroy' +import assert from 'assert' const debug = Debug('cypress:server:socket-ct') export class SocketCt extends SocketBase { + #destroyAutPromise?: dfd.DeferredPromise + constructor (config: Record) { super(config) @@ -20,9 +24,22 @@ export class SocketCt extends SocketBase { startListening (server: DestroyableHttpServer, automation, config, options) { return super.startListening(server, automation, config, options, { - onSocketConnection (socket: socketIo.SocketIOServer) { + onSocketConnection: (socket: Socket) => { debug('do onSocketConnection') + + socket.on('aut:destroy:complete', () => { + assert(this.#destroyAutPromise) + this.#destroyAutPromise.resolve() + }) }, }) } + + destroyAut () { + this.#destroyAutPromise = dfd() + + this.toRunner('aut:destroy:init') + + return this.#destroyAutPromise.promise + } } diff --git a/packages/server/test/unit/config_spec.js b/packages/server/test/unit/config_spec.js index 8823589a35d4..513afd0f03e7 100644 --- a/packages/server/test/unit/config_spec.js +++ b/packages/server/test/unit/config_spec.js @@ -1488,6 +1488,7 @@ describe('lib/config', () => { experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalSingleTabRunMode: { value: false, from: 'default' }, experimentalSessionAndOrigin: { value: false, from: 'default' }, experimentalSourceRewriting: { value: false, from: 'default' }, fileServerFolder: { value: '', from: 'default' }, @@ -1577,6 +1578,7 @@ describe('lib/config', () => { experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalSingleTabRunMode: { value: false, from: 'default' }, experimentalSessionAndOrigin: { value: false, from: 'default' }, experimentalSourceRewriting: { value: false, from: 'default' }, env: { diff --git a/system-tests/__snapshots__/component_testing_spec.ts.js b/system-tests/__snapshots__/component_testing_spec.ts.js index 940eba9f5353..ad7616d0ddd6 100644 --- a/system-tests/__snapshots__/component_testing_spec.ts.js +++ b/system-tests/__snapshots__/component_testing_spec.ts.js @@ -531,3 +531,210 @@ exports['React major versions with Vite executes all of the tests for React v18 ` + +exports['experimentalSingleTabRunMode / executes all specs in a single tab'] = ` +We detected that you have versions of dependencies that are not officially supported: + + - \`webpack\`. Expected >=4.0.0 || >=5.0.0 but dependency was not found. + +If you're experiencing problems, downgrade dependencies and restart Cypress. + + 30 modules + +==================================================================================================== + + (Run Starting) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Cypress: 1.2.3 │ + │ Browser: FooBrowser 88 │ + │ Specs: 4 found (1_fails.cy.js, 2_foo.cy.js, 3_retries.cy.js, 999_final.cy.js) │ + │ Searched: **/*.cy.js │ + │ Experiments: experimentalSingleTabRunMode=true │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: 1_fails.cy.js (1 of 4) + + + simple failing spec + 1) fails + 2) fails again + + + 0 passing + 2 failing + + 1) simple failing spec + fails: + + AssertionError: expected 1 to equal 2 + + expected - actual + + -1 + +2 + + [stack trace lines] + + 2) simple failing spec + fails again: + + AssertionError: expected 1 to equal 3 + + expected - actual + + -1 + +3 + + [stack trace lines] + + + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 2 │ + │ Passing: 0 │ + │ Failing: 2 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 2 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: 1_fails.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Screenshots) + + - /XXX/XXX/XXX/cypress/screenshots/1_fails.cy.js/simple failing spec -- fails (fai (1280x720) + led).png + - /XXX/XXX/XXX/cypress/screenshots/1_fails.cy.js/simple failing spec -- fails agai (1280x720) + n (failed).png + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/1_fails.cy.js.mp4 (X second) + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: 2_foo.cy.js (2 of 4) + + + component + ✓ passes + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: 2_foo.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/2_foo.cy.js.mp4 (X second) + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: 3_retries.cy.js (3 of 4) + + + retries + (Attempt 1 of 2) passes after 1 failure + ✓ passes after 1 failure + + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: 3_retries.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/3_retries.cy.js.mp4 (X second) + + +──────────────────────────────────────────────────────────────────────────────────────────────────── + + Running: 999_final.cy.js (4 of 4) + + + ✓ verifies AUT is destroyed after each spec + + 1 passing + + + (Results) + + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ Tests: 1 │ + │ Passing: 1 │ + │ Failing: 0 │ + │ Pending: 0 │ + │ Skipped: 0 │ + │ Screenshots: 0 │ + │ Video: true │ + │ Duration: X seconds │ + │ Spec Ran: 999_final.cy.js │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + + + (Video) + + - Started processing: Compressing to 32 CRF + - Finished processing: /XXX/XXX/XXX/cypress/videos/999_final.cy.js.mp4 (X second) + + +==================================================================================================== + + (Run Finished) + + + Spec Tests Passing Failing Pending Skipped + ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ + │ ✖ 1_fails.cy.js XX:XX 2 - 2 - - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✔ 2_foo.cy.js XX:XX 1 1 - - - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✔ 3_retries.cy.js XX:XX 1 1 - - - │ + ├────────────────────────────────────────────────────────────────────────────────────────────────┤ + │ ✔ 999_final.cy.js XX:XX 1 1 - - - │ + └────────────────────────────────────────────────────────────────────────────────────────────────┘ + ✖ 1 of 4 failed (25%) XX:XX 5 3 2 - - + + +` diff --git a/system-tests/projects/component-tests/cypress/component-tests/fails.spec.js b/system-tests/projects/component-tests/cypress/component-tests/fails.spec.js index 842cf72208e9..f83991c974bd 100644 --- a/system-tests/projects/component-tests/cypress/component-tests/fails.spec.js +++ b/system-tests/projects/component-tests/cypress/component-tests/fails.spec.js @@ -1,4 +1,4 @@ -describe('simple passing spec', () => { +describe('simple failing spec', () => { it('fails', () => { expect(1).to.eq(2) }) diff --git a/system-tests/projects/experimentalSingleTabRunMode/cypress.config.js b/system-tests/projects/experimentalSingleTabRunMode/cypress.config.js new file mode 100644 index 000000000000..dcd61a9e1b56 --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/cypress.config.js @@ -0,0 +1,15 @@ +const { defineConfig } = require('cypress') + +module.exports = defineConfig({ + e2e: { + setupNodeEvents: (on, config) => config, + // invalid - used for e2e testing to verify error is shown + experimentalSingleTabRunMode: true, + }, + component: { + experimentalSingleTabRunMode: true, + devServer: { + bundler: 'webpack', + }, + }, +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component-index.html b/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component-index.html new file mode 100644 index 000000000000..ac6e79fd83df --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component.js b/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component.js new file mode 100644 index 000000000000..e26c618eed8c --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/cypress/support/component.js @@ -0,0 +1,11 @@ +before(() => { + const eventManager = window.top.getEventManager() + + eventManager.ws.once('aut:destroy:init', () => { + if (!eventManager.autDestroyedCount) { + eventManager.autDestroyedCount = 0 + } + + eventManager.autDestroyedCount += 1 + }) +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/index.html b/system-tests/projects/experimentalSingleTabRunMode/index.html new file mode 100644 index 000000000000..0aac47a2fd9e --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/index.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +

Hello World

+ + \ No newline at end of file diff --git a/system-tests/projects/experimentalSingleTabRunMode/src/1_fails.cy.js b/system-tests/projects/experimentalSingleTabRunMode/src/1_fails.cy.js new file mode 100644 index 000000000000..f83991c974bd --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/src/1_fails.cy.js @@ -0,0 +1,9 @@ +describe('simple failing spec', () => { + it('fails', () => { + expect(1).to.eq(2) + }) + + it('fails again', () => { + expect(1).to.eq(3) + }) +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/src/2_foo.cy.js b/system-tests/projects/experimentalSingleTabRunMode/src/2_foo.cy.js new file mode 100644 index 000000000000..88047ebb7a6b --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/src/2_foo.cy.js @@ -0,0 +1,5 @@ +describe('component', () => { + it('passes', () => { + expect('foo').to.eq('foo') + }) +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/src/3_retries.cy.js b/system-tests/projects/experimentalSingleTabRunMode/src/3_retries.cy.js new file mode 100644 index 000000000000..36485124c142 --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/src/3_retries.cy.js @@ -0,0 +1,12 @@ +describe('retries', () => { + let i = 0 + + it('passes after 1 failure', { retries: 1 }, () => { + if (i === 0) { + i++ + expect(1).to.eq(2) + } + + expect(1).to.eq(1) + }) +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/src/999_final.cy.js b/system-tests/projects/experimentalSingleTabRunMode/src/999_final.cy.js new file mode 100644 index 000000000000..6c342f818fce --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/src/999_final.cy.js @@ -0,0 +1,5 @@ +it('verifies AUT is destroyed after each spec', () => { + // there are three other specs that have run by now + // so the aut destroy count should be 3 + expect(window.top.getEventManager().autDestroyedCount).to.eq(3) +}) diff --git a/system-tests/projects/experimentalSingleTabRunMode/webpack.config.js b/system-tests/projects/experimentalSingleTabRunMode/webpack.config.js new file mode 100644 index 000000000000..4ba52ba2c8df --- /dev/null +++ b/system-tests/projects/experimentalSingleTabRunMode/webpack.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/system-tests/test/component_testing_spec.ts b/system-tests/test/component_testing_spec.ts index e8e1c2394259..fbe38e3786e3 100644 --- a/system-tests/test/component_testing_spec.ts +++ b/system-tests/test/component_testing_spec.ts @@ -124,3 +124,16 @@ describe(`Angular CLI major versions`, () => { }) } }) + +describe('experimentalSingleTabRunMode', function () { + systemTests.setup() + + systemTests.it('executes all specs in a single tab', { + project: 'experimentalSingleTabRunMode', + testingType: 'component', + spec: '**/*.cy.js', + browser: 'chrome', + snapshot: true, + expectedExitCode: 2, + }) +})