mirror of
https://github.com/cypress-io/cypress.git
synced 2026-01-05 14:09:46 -06:00
feat: experimental single tab run mode for component testing (#23104)
* revive logic to run CT in a single tab * add feature flag: experimentalSingleTabRunMode * remove log * reset browser state between tests * document single tab run mode experiment; * add system test for experimental run mode * fix snapshots * use more simple project for testing * additional guard; * fix test * Apply suggestions from code review Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com> * destroy aut after each spec * update snapshot * fix types * add experiment flag error * add warning when using experimental flag with e2e * build binaries for experimentalSingleTabRunMode feature * build binaries take 2 * make error message more open ended * destroy AUT later in run mode lifecycle * add additional assertion around experimental flag * simplify error * remove test code from production code Co-authored-by: Emily Rohrbough <emilyrohrbough@users.noreply.github.com>
This commit is contained in:
@@ -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 >>
|
||||
|
||||
5
cli/types/cypress.d.ts
vendored
5
cli/types/cypress.d.ts
vendored
@@ -3062,6 +3062,11 @@ declare namespace Cypress {
|
||||
interface ComponentConfigOptions<ComponentDevServerOpts = any> extends Omit<CoreConfigOptions, 'baseUrl' | 'experimentalSessionAndOrigin'> {
|
||||
devServer: DevServerFn<ComponentDevServerOpts> | DevServerConfigOptions
|
||||
devServerConfig?: ComponentDevServerOpts
|
||||
/**
|
||||
* Runs all component specs in a single tab, trading spec isolation for faster run mode execution.
|
||||
* @default false
|
||||
*/
|
||||
experimentalSingleTabRunMode?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,17 @@ export const StubWebsocket = new Proxy<Socket>(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 = () => {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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<Array<string | Record<string, any>>>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<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) {
|
||||
|
||||
@@ -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<AllCypressErrorNames[]> = [
|
||||
'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<DriverConfigOption> = [
|
||||
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<DriverConfigOption | RuntimeConfigOption> = [
|
||||
/**
|
||||
* Values not allowed in 10.X+ in the root, e2e and component config
|
||||
*/
|
||||
export const breakingOptions: Array<BreakingOption> = [
|
||||
export const breakingOptions: Readonly<BreakingOption[]> = [
|
||||
{
|
||||
name: 'blacklistHosts',
|
||||
errorKey: 'RENAMED_CONFIG_OPTION',
|
||||
@@ -592,7 +604,7 @@ export const breakingOptions: Array<BreakingOption> = [
|
||||
newName: 'specPattern',
|
||||
isWarning: false,
|
||||
},
|
||||
]
|
||||
] as const
|
||||
|
||||
export const breakingRootOptions: Array<BreakingOption> = [
|
||||
{
|
||||
@@ -645,6 +657,12 @@ export const breakingRootOptions: Array<BreakingOption> = [
|
||||
|
||||
export const testingTypeBreakingOptions: { e2e: Array<BreakingOption>, component: Array<BreakingOption> } = {
|
||||
e2e: [
|
||||
{
|
||||
name: 'experimentalSingleTabRunMode',
|
||||
errorKey: 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE',
|
||||
isWarning: false,
|
||||
testingTypes: ['e2e'],
|
||||
},
|
||||
{
|
||||
name: 'indexHtmlFile',
|
||||
errorKey: 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_E2E',
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">We detected that you have versions of dependencies that are not officially supported:<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4f5666"> - <span style="color:#e05561"><span style="color:#4ec4ff">`vite`. Expected >=2.0.0, found 1.0.0.<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#4f5666"> - <span style="color:#e05561"><span style="color:#4ec4ff">`vite`. Expected ^=2.0.0 || ^=3.0.0, found 1.0.0.<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">If you're experiencing problems, downgrade dependencies and restart Cypress.<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>
|
||||
|
||||
40
packages/errors/__snapshot-html__/EXPERIMENTAL_SINGLE_TAB_RUN_MODE.html
generated
Normal file
40
packages/errors/__snapshot-html__/EXPERIMENTAL_SINGLE_TAB_RUN_MODE.html
generated
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Courier+Prime&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
|
||||
body {
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
padding: 0 1em;
|
||||
line-height: 1.4;
|
||||
color: #eee;
|
||||
background-color: #111;
|
||||
}
|
||||
pre {
|
||||
padding: 0 0;
|
||||
margin: 0 0;
|
||||
font-family: "Courier Prime", Courier, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 5px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body><pre><span style="color:#e05561">The <span style="color:#e5e510">experimentalSingleTabRunMode<span style="color:#e05561"> experiment is currently only supported for Component Testing.<span style="color:#e6e6e6">
|
||||
<span style="color:#e05561"><span style="color:#e6e6e6">
|
||||
<span style="color:#e05561">If you have feedback about the experiment, please join the discussion here: http://on.cypress.io/single-tab-run-mode<span style="color:#e6e6e6"></span></span></span></span></span></span></span></span>
|
||||
</pre></body></html>
|
||||
@@ -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.
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<h1
|
||||
v-if="baseError.title"
|
||||
class="font-medium leading-snug text-32px text-gray-900"
|
||||
data-testid="error-header"
|
||||
data-cy="error-header"
|
||||
>
|
||||
<slot name="header">
|
||||
{{ baseError.title }}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -48,4 +48,8 @@ export class ServerCt extends ServerBase<SocketCt> {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
destroyAut () {
|
||||
return this.socket.destroyAut()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,4 +609,8 @@ export class SocketBase {
|
||||
close () {
|
||||
return this._io?.close()
|
||||
}
|
||||
|
||||
changeToUrl (url: string) {
|
||||
return this.toRunner('change:to:url', url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void>
|
||||
|
||||
constructor (config: Record<string, any>) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 - -
|
||||
|
||||
|
||||
`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
describe('simple passing spec', () => {
|
||||
describe('simple failing spec', () => {
|
||||
it('fails', () => {
|
||||
expect(1).to.eq(2)
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>Components App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div data-cy-root></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,9 @@
|
||||
describe('simple failing spec', () => {
|
||||
it('fails', () => {
|
||||
expect(1).to.eq(2)
|
||||
})
|
||||
|
||||
it('fails again', () => {
|
||||
expect(1).to.eq(3)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
describe('component', () => {
|
||||
it('passes', () => {
|
||||
expect('foo').to.eq('foo')
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {}
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user