diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 7f0856072e58..2b21aece66fc 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -27,6 +27,7 @@ mainBuildFilters: &mainBuildFilters branches: only: - develop + - /^release\/\d+\.\d+\.\d+$/ - 'ryanm/fix/v8-improvements' # usually we don't build Mac app - it takes a long time @@ -38,17 +39,16 @@ macWorkflowFilters: &darwin-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: - pattern: "-release$" - value: << pipeline.git.branch >> - + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: - pattern: "-release$" - value: << pipeline.git.branch >> + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> # uncomment & add to the branch conditions below to disable the main linux # flow if we don't want to test it for a certain branch @@ -65,9 +65,8 @@ windowsWorkflowFilters: &windows-workflow-filters - equal: [ develop, << pipeline.git.branch >> ] - equal: [ 'ryanm/fix/v8-improvements', << pipeline.git.branch >> ] - matches: - pattern: "-release$" - value: << pipeline.git.branch >> - + pattern: /^release\/\d+\.\d+\.\d+$/ + value: << pipeline.git.branch >> executors: # the Docker image with Cypress dependencies and Chrome browser cy-doc: @@ -130,7 +129,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "ryanm/fix/v8-improvements" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "ryanm/fix/v8-improvements" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi @@ -461,10 +460,6 @@ commands: description: chrome channel to install type: string default: '' - experimentalSessionAndOrigin: - description: experimental flag to apply - type: boolean - default: false steps: - restore_cached_workspace - when: @@ -488,13 +483,8 @@ commands: if [[ -v MAIN_RECORD_KEY ]]; then # internal PR - if <>; then - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run-experimentalSessionAndOrigin --record --parallel --group 5x-driver-<>-experimentalSessionAndOrigin --browser <> - else - CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ - yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> - fi + CYPRESS_RECORD_KEY=$MAIN_RECORD_KEY \ + yarn cypress:run --record --parallel --group 5x-driver-<> --browser <> else # external PR TESTFILES=$(circleci tests glob "cypress/e2e/**/*.cy.*" | circleci tests split --total=$CIRCLE_NODE_TOTAL) @@ -503,11 +493,7 @@ commands: if [[ -z "$TESTFILES" ]]; then echo "Empty list of test files" fi - if <>; then - yarn cypress:run-experimentalSessionAndOrigin --browser <> --spec $TESTFILES - else - yarn cypress:run --browser <> --spec $TESTFILES - fi + yarn cypress:run --browser <> --spec $TESTFILES fi working_directory: packages/driver - verify-mocha-results @@ -1657,58 +1643,12 @@ jobs: driver-integration-tests-webkit: <<: *defaults + resource_class: medium+ parallelism: 5 steps: - run-driver-integration-tests: browser: webkit - driver-integration-tests-chrome-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome - install-chrome-channel: stable - experimentalSessionAndOrigin: true - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: chrome:beta - install-chrome-channel: beta - experimentalSessionAndOrigin: true - - driver-integration-tests-firefox-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: firefox - experimentalSessionAndOrigin: true - - driver-integration-tests-electron-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: electron - experimentalSessionAndOrigin: true - - driver-integration-tests-webkit-experimentalSessionAndOrigin: - <<: *defaults - resource_class: medium - parallelism: 5 - steps: - - run-driver-integration-tests: - browser: webkit - experimentalSessionAndOrigin: true - run-reporter-component-tests-chrome: <<: *defaults parameters: @@ -2482,26 +2422,6 @@ linux-x64-workflow: &linux-x64-workflow context: test-runner:cypress-record-key requires: - build - - driver-integration-tests-chrome-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-firefox-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-electron-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - - driver-integration-tests-webkit-experimentalSessionAndOrigin: - context: test-runner:cypress-record-key - requires: - - build - run-frontend-shared-component-tests-chrome: context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true @@ -2606,10 +2526,6 @@ linux-x64-workflow: &linux-x64-workflow - driver-integration-tests-chrome - driver-integration-tests-chrome-beta - driver-integration-tests-electron - - driver-integration-tests-firefox-experimentalSessionAndOrigin - - driver-integration-tests-chrome-experimentalSessionAndOrigin - - driver-integration-tests-chrome-beta-experimentalSessionAndOrigin - - driver-integration-tests-electron-experimentalSessionAndOrigin - system-tests-non-root - system-tests-firefox - system-tests-electron diff --git a/cli/package.json b/cli/package.json index a9f70660e5ec..d485b7e34f3c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -116,7 +116,7 @@ "cypress": "bin/cypress" }, "engines": { - "node": ">=12.0.0" + "node": "^14.0.0 || ^16.0.0 || >=18.0.0" }, "types": "types", "exports": { diff --git a/cli/types/cypress-npm-api.d.ts b/cli/types/cypress-npm-api.d.ts index 394c12a7a015..a7051b7f1462 100644 --- a/cli/types/cypress-npm-api.d.ts +++ b/cli/types/cypress-npm-api.d.ts @@ -173,7 +173,7 @@ declare namespace CypressCommandLine { title: string[] state: string body: string - /** + /** * Error string as it's presented in console if the test fails */ displayError: string | null diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 429513bbcdc6..515c4142ca52 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -50,6 +50,9 @@ declare namespace Cypress { interface CommandFnWithOriginalFnAndSubject { (this: Mocha.Context, originalFn: CommandOriginalFnWithSubject, prevSubject: S, ...args: Parameters): ReturnType | void } + interface QueryFn { + (this: Command, ...args: Parameters): (subject: any) => any + } interface ObjectLike { [key: string]: any } @@ -128,6 +131,58 @@ declare namespace Cypress { unsupportedVersion?: boolean } + interface Ensure { + /** + * Throws an error if `subject` is not one of the passed in `type`s. + */ + isType(subject: any, type: PrevSubject[], commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a DOM element. + */ + isElement(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a `document`. + */ + isDocument(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a `window`. + */ + isWindow(subject: any, commandName: string, cy: Chainable): void + + /** + * Throws an error if `subject` is not a DOM element attached to the application under test. + */ + isAttached(subject: any, commandName: string, cy: Chainable, onFail?: Log): void + + /** + * Throws an error if `subject` is a disabled DOM element. + */ + isNotDisabled(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a DOM element hidden by any of its parent elements. + */ + isNotHiddenByAncestors(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a read-only form element. + */ + isNotReadonly(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is a read-only form element. + */ + isScrollable(subject: any, commandName: string, onFail?: Log): void + + /** + * Throws an error if `subject` is not a DOM element visible in the AUT. + */ + isVisible(subject: any, commandName: string, onFail?: Log): void + } + interface LocalStorage { /** * Called internally to clear `localStorage` in two situations. @@ -305,6 +360,12 @@ declare namespace Cypress { */ sinon: sinon.SinonStatic + /** + * Utility functions for ensuring various properties about a subject. + * @see https://on.cypress.io/custom-queries + */ + ensure: Ensure + /** * Cypress version string. i.e. "1.1.2" * @see https://on.cypress.io/version @@ -496,30 +557,92 @@ declare namespace Cypress { */ log(options: Partial): Log - /** - * @see https://on.cypress.io/api/commands - */ Commands: { + /** + * Add a custom command + * @see https://on.cypress.io/api/commands + */ add(name: T, fn: CommandFn): void + + /** + * Add a custom parent command + * @see https://on.cypress.io/api/commands#Parent-Commands + */ add(name: T, options: CommandOptions & {prevSubject: false}, fn: CommandFn): void + + /** + * Add a custom child command + * @see https://on.cypress.io/api/commands#Child-Commands + */ add(name: T, options: CommandOptions & {prevSubject: true}, fn: CommandFnWithSubject): void + + /** + * Add a custom child or dual command + * @see https://on.cypress.io/api/commands#Validations + */ add( name: T, options: CommandOptions & { prevSubject: S | ['optional'] }, fn: CommandFnWithSubject, ): void + + /** + * Add a custom command that allows multiple types as the prevSubject + * @see https://on.cypress.io/api/commands#Validations#Allow-Multiple-Types + */ add( name: T, options: CommandOptions & { prevSubject: S[] }, fn: CommandFnWithSubject[S]>, ): void + + /** + * Add one or more custom commands + * @see https://on.cypress.io/api/commands + */ addAll(fns: CommandFns): void + + /** + * Add one or more custom parent commands + * @see https://on.cypress.io/api/commands#Parent-Commands + */ addAll(options: CommandOptions & {prevSubject: false}, fns: CommandFns): void + + /** + * Add one or more custom child commands + * @see https://on.cypress.io/api/commands#Child-Commands + */ addAll(options: CommandOptions & { prevSubject: true }, fns: CommandFnsWithSubject): void + + /** + * Add one or more custom commands that validate their prevSubject + * @see https://on.cypress.io/api/commands#Validations + */ addAll( options: CommandOptions & { prevSubject: S | ['optional'] }, fns: CommandFnsWithSubject, ): void + + /** + * Add one or more custom commands that allow multiple types as their prevSubject + * @see https://on.cypress.io/api/commands#Allow-Multiple-Types + */ addAll( options: CommandOptions & { prevSubject: S[] }, fns: CommandFnsWithSubject[S]>, ): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ overwrite(name: T, fn: CommandFnWithOriginalFn): void + + /** + * Overwrite an existing Cypress command with a new implementation + * @see https://on.cypress.io/api/commands#Overwrite-Existing-Commands + */ overwrite(name: T, fn: CommandFnWithOriginalFnAndSubject): void + + /** + * Add a custom query + * @see https://on.cypress.io/api/custom-queries + */ + addQuery(name: T, fn: QueryFn): void } /** @@ -527,16 +650,6 @@ declare namespace Cypress { */ Cookies: { debug(enabled: boolean, options?: Partial): void - /** - * @deprecated Use `cy.session()` instead. - * @see https://on.cypress.io/session - */ - preserveOnce(...names: string[]): void - /** - * @deprecated Use `cy.session()` instead. - * @see https://on.cypress.io/session - */ - defaults(options: Partial): CookieDefaults } /** @@ -635,13 +748,6 @@ declare namespace Cypress { defaults(options: Partial): void } - /** - * @see https://on.cypress.io/api/api-server - */ - Server: { - defaults(options: Partial): void - } - /** * @see https://on.cypress.io/screenshot-api */ @@ -809,7 +915,7 @@ declare namespace Cypress { clear(options?: Partial): Chainable /** - * Clear a specific browser cookie for the current superdomain or for the domain specified. + * Clear a specific browser cookie for the current hostname or for the domain specified. * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear a specific cookie inside a single test. * * @see https://on.cypress.io/clearcookie @@ -817,7 +923,7 @@ declare namespace Cypress { clearCookie(name: string, options?: CookieOptions): Chainable /** - * Clear all browser cookies for the current superdomain or for the domain specified. + * Clear all browser cookies for the current hostname or for the domain specified. * Cypress automatically clears all cookies before each test to prevent state from being shared across tests. You shouldn't need to use this command unless you're using it to clear all cookies or specific cookies inside a single test. * * @see https://on.cypress.io/clearcookies @@ -1147,8 +1253,6 @@ declare namespace Cypress { /** * Save/Restore browser Cookies, LocalStorage, and SessionStorage data resulting from the supplied `setup` function. * - * Only available if the `experimentalSessionAndOrigin` config option is enabled. - * * @see https://on.cypress.io/session */ session(id: string | object, setup: () => void, options?: SessionOptions): Chainable @@ -1310,14 +1414,14 @@ declare namespace Cypress { get(alias: string, options?: Partial): Chainable /** - * Get a browser cookie by its name for the current superdomain or for the domain specified. + * Get a browser cookie by its name for the current hostname or for the domain specified. * * @see https://on.cypress.io/getcookie */ getCookie(name: string, options?: CookieOptions): Chainable /** - * Get all of the browser cookies for the current superdomain or for the domain specified. + * Get all of the browser cookies for the current hostname or for the domain specified. * * @see https://on.cypress.io/getcookies */ @@ -1491,6 +1595,13 @@ declare namespace Cypress { */ not(selector: string, options?: Partial): Chainable + /** + * Invoke a command synchronously, without using the command queue. + * + * @see https://on.cypress.io/custom-queries + */ + now(name: string, ...args: any[]): Promise | ((subject: any) => any) + /** * These events come from Cypress as it issues commands and reacts to their state. These are all useful to listen to for debugging purposes. * @see https://on.cypress.io/catalog-of-events#App-Events @@ -1739,66 +1850,6 @@ declare namespace Cypress { */ root(options?: Partial): Chainable> // can't do better typing unless we ignore the `.within()` case - /** - * @deprecated Use `cy.intercept()` instead. - * - * Use `cy.route()` to manage the behavior of network requests. - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route('https://localhost:7777/users', [{id: 1, name: 'Pat'}]) - */ - route(url: string | RegExp, response?: string | object): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Spy or stub request with specific method and url. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * // spy on POST /todos requests - * cy.route('POST', '/todos').as('add-todo') - */ - route(method: string, url: string | RegExp, response?: string | object): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Set a route by returning an object literal from a callback function. - * Functions that return a Promise will automatically be awaited. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route(() => { - * // your logic here - * // return an appropriate routing object here - * return { - * method: 'POST', - * url: '/comments', - * response: this.commentsFixture - * } - * }) - */ - route(fn: () => RouteOptions): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Spy or stub a given route. - * - * @see https://on.cypress.io/route - * @example - * cy.server() - * cy.route({ - * method: 'DELETE', - * url: '/users', - * status: 412, - * delay: 1000 - * // and other options, see documentation - * }) - */ - route(options: Partial): Chainable - /** * Take a screenshot of the application under test and the Cypress Command Log. * @@ -1844,26 +1895,6 @@ declare namespace Cypress { */ select(valueOrTextOrIndex: string | number | Array, options?: Partial): Chainable - /** - * @deprecated Use `cy.intercept()` instead. - * - * Start a server to begin routing responses to `cy.route()` and `cy.request()`. - * - * @example - * // start server - * cy.server() - * // get default server options - * cy.server().should((server) => { - * expect(server.delay).to.eq(0) - * expect(server.method).to.eq('GET') - * expect(server.status).to.eq(200) - * // and many others options - * }) - * - * @see https://on.cypress.io/server - */ - server(options?: Partial): Chainable - /** * Set a browser cookie. * @@ -2465,10 +2496,6 @@ declare namespace Cypress { type Agent = SinonSpyAgent & T - interface CookieDefaults { - preserve: string | string[] | RegExp | ((cookie: Cookie) => boolean) - } - interface Failable { /** * Whether to fail on response codes other than 2xx and 3xx @@ -2698,7 +2725,7 @@ declare namespace Cypress { interface CookieOptions extends Partial { /** * Domain to set cookies on or get cookies from - * @default superdomain of the current app under test + * @default hostname of the current app under test */ domain?: string } @@ -2878,8 +2905,7 @@ declare namespace Cypress { */ supportFile: string | false /** - * The test isolation ensures a clean browser context between tests. This option is only available when - * `experimentalSessionAndOrigin=true`. + * The test isolation ensures a clean browser context between tests. * * Cypress will always reset/clear aliases, intercepts, clock, and viewport before each test * to ensure a clean test slate; i.e. this configuration only impacts the browser context. @@ -2887,23 +2913,23 @@ declare namespace Cypress { * Note: the [`cy.session()`](https://on.cypress.io/session) command will inherent this value to determine whether * or not the page is cleared when the command executes. This command is only available in end-to-end testing. * - * - on - The page is cleared before each test. Cookies, local storage and session storage in all domains are cleared + * - true - The page is cleared before each test. Cookies, local storage and session storage in all domains are cleared * before each test. The `cy.session()` command will also clear the page and current browser context when creating * or restoring the browser session. - * - off - The current browser state will persist between tests. The page does not clear before the test and cookies, local + * - false - The current browser state will persist between tests. The page does not clear before the test and cookies, local * storage and session storage will be available in the next test. The `cy.session()` command will only clear the * current browser context when creating or restoring the browser session - the current page will not clear. * * Tradeoffs: * Turning test isolation off may improve performance of end-to-end tests, however, previous tests could impact the * browser state of the next test and cause inconsistency when using .only(). Be mindful to write isolated tests when - * test isolation is off. If a test in the suite impacts the state of other tests and it were to fail, you could see + * test isolation is false. If a test in the suite impacts the state of other tests and it were to fail, you could see * misleading errors in later tests which makes debugging clunky. See the [documentation](https://on.cypress.io/test-isolation) * for more information. * - * @default null, when experimentalSessionAndOrigin=false. The default is 'on' when experimentalSessionAndOrigin=true. + * @default true */ - testIsolation: null | 'on' | 'off' + testIsolation: boolean /** * Path to folder where videos will be saved after a headless or CI run * @default "cypress/videos" @@ -2964,11 +2990,6 @@ declare namespace Cypress { * @default false */ experimentalInteractiveRunEvents: boolean - /** - * Enables cross-origin and improved session support, including the `cy.origin` and `cy.session` commands. See https://on.cypress.io/origin and https://on.cypress.io/session. - * @default false - */ - experimentalSessionAndOrigin: boolean /** * Whether Cypress will search for and replace obstructive code in third party .js or .html files. * NOTE: Setting this flag to true removes Subresource Integrity (SRI). @@ -3065,6 +3086,11 @@ declare namespace Cypress { * @default false */ experimentalRunAllSpecs?: boolean + /** + * Enables support for require/import within cy.origin. + * @default false + */ + experimentalOriginDependencies?: boolean } /** @@ -3140,19 +3166,17 @@ declare namespace Cypress { socketIoRoute: string spec: Cypress['spec'] | null specs: Array - xhrRoute: string - xhrUrl: string } interface SuiteConfigOverrides extends Partial< - Pick + Pick >, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number } interface TestConfigOverrides extends Partial< - Pick + Pick >, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number @@ -3209,7 +3233,7 @@ declare namespace Cypress { } } - interface ComponentConfigOptions extends Omit { + interface ComponentConfigOptions extends Omit { devServer: DevServerFn | DevServerConfigOptions devServerConfig?: ComponentDevServerOpts /** @@ -3398,28 +3422,6 @@ declare namespace Cypress { interval: number } - /** - * Setting default options for cy.server() - * @see https://on.cypress.io/server - */ - interface ServerOptions { - delay: number - method: HttpMethod - status: number - headers: object - response: any - onRequest(...args: any[]): void - onResponse(...args: any[]): void - onAbort(...args: any[]): void - enable: boolean - force404: boolean - urlMatchingOptions: object - ignore(xhr: Request): void - onAnyRequest(route: RouteOptions, proxy: any): void - onAnyResponse(route: RouteOptions, proxy: any): void - onAnyAbort(route: RouteOptions, proxy: any): void - } - interface Session { // Clear all saved sessions and re-run the current spec file. clearAllSavedSessions: () => Promise @@ -5839,7 +5841,7 @@ declare namespace Cypress { * Useful for debugging purposes if you're confused about the order in which commands will execute. * @see https://on.cypress.io/catalog-of-events#App-Events */ - (action: 'command:enqueued', fn: (command: EnqueuedCommand) => void): Cypress + (action: 'command:enqueued', fn: (command: EnqueuedCommandAttributes) => void): Cypress /** * Fires when cy begins actually running and executing your command. * Useful for debugging and understanding how the command queue is async. @@ -5967,7 +5969,7 @@ declare namespace Cypress { sameSite?: SameSiteStatus } - interface EnqueuedCommand { + interface EnqueuedCommandAttributes { id: string name: string args: any[] @@ -5975,9 +5977,17 @@ declare namespace Cypress { chainerId: string injected: boolean userInvocationStack?: string + query?: boolean fn(...args: any[]): any } + interface Command { + get(attr: K): EnqueuedCommandAttributes[K] + get(): EnqueuedCommandAttributes + set(key: K, value: EnqueuedCommandAttributes[K]): Log + set(options: Partial): Log + } + interface Exec { code: number stdout: string @@ -6056,28 +6066,6 @@ declare namespace Cypress { viewportHeight: number } - interface WaitXHR { - duration: number - id: string - method: HttpMethod - request: { - body: string | ObjectLike - headers: ObjectLike - } - requestBody: WaitXHR['request']['body'] - requestHeaders: WaitXHR['request']['headers'] - response: { - body: string | ObjectLike - headers: ObjectLike - } - responseBody: WaitXHR['response']['body'] - responseHeaders: WaitXHR['response']['headers'] - status: number - statusMessage: string - url: string - xhr: XMLHttpRequest - } - type Encodings = 'ascii' | 'base64' | 'binary' | 'hex' | 'latin1' | 'utf8' | 'utf-8' | 'ucs2' | 'ucs-2' | 'utf16le' | 'utf-16le' | null type PositionType = 'topLeft' | 'top' | 'topRight' | 'left' | 'center' | 'right' | 'bottomLeft' | 'bottom' | 'bottomRight' type ViewportPreset = 'macbook-16' | 'macbook-15' | 'macbook-13' | 'macbook-11' | 'ipad-2' | 'ipad-mini' | 'iphone-xr' | 'iphone-x' | 'iphone-6+' | 'iphone-se2' | 'iphone-8' | 'iphone-7' | 'iphone-6' | 'iphone-5' | 'iphone-4' | 'iphone-3' | 'samsung-s10' | 'samsung-note9' diff --git a/cli/types/tests/actions.ts b/cli/types/tests/actions.ts index 831aaee0fe84..6ebf7799a69f 100644 --- a/cli/types/tests/actions.ts +++ b/cli/types/tests/actions.ts @@ -46,7 +46,7 @@ Cypress.on('scrolled', ($el) => { }) Cypress.on('command:enqueued', (command) => { - command // $ExpectType EnqueuedCommand + command // $ExpectType EnqueuedCommandAttributes }) Cypress.on('command:start', (command) => { diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index c9cd6ef287cb..7bebba4080ab 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -39,7 +39,7 @@ namespace CypressConfigTests { Cypress.config({ e2e: { baseUrl: '.', }}) // $ExpectError Cypress.config({ component: { baseUrl: '.', devServer: () => ({} as any) } }) // $ExpectError Cypress.config({ e2e: { indexHtmlFile: 'index.html' } }) // $ExpectError - Cypress.config({ testIsolation: 'off' }) // $ExpectError + Cypress.config({ testIsolation: false }) // $ExpectError Cypress.config('taskTimeout') // $ExpectType number Cypress.config('includeShadowDom') // $ExpectType boolean @@ -69,6 +69,7 @@ namespace CypressIsCyTests { declare namespace Cypress { interface Chainable { newCommand: (arg: string) => Chainable + newQuery: (arg: string) => Chainable } } @@ -291,6 +292,27 @@ namespace CypressCommandsTests { return originalFn(element, text, options) }) + + Cypress.Commands.addQuery('newQuery', function(arg) { + this // $ExpectType Command + arg // $ExpectType string + return () => 3 + }) +} + +namespace CypressNowTest { + cy.now('get') // $ExpectType Promise | ((subject: any) => any) +} + +namespace CypressEnsuresTest { + Cypress.ensure.isType('', ['optional', 'element'], 'newQuery', cy) // $ExpectType void + Cypress.ensure.isElement('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isWindow('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isDocument('', 'newQuery', cy) // $ExpectType void + + Cypress.ensure.isAttached('', 'newQuery', cy) // $ExpectType void + Cypress.ensure.isNotDisabled('', 'newQuery') // $ExpectType void + Cypress.ensure.isVisible('', 'newQuery') // $ExpectType void } namespace CypressLogsTest { @@ -868,7 +890,7 @@ namespace CypressTestConfigOverridesTests { retries: { run: 3 } // $ExpectError }, () => { }) it('test', { - testIsolation: 'off', // $ExpectError + testIsolation: false, // $ExpectError }, () => { }) it.skip('test', {}, () => {}) @@ -887,7 +909,7 @@ namespace CypressTestConfigOverridesTests { }, () => {}) describe('suite', { - testIsolation: 'off', + testIsolation: false, }, () => {}) context('suite', {}, () => {}) diff --git a/cli/types/tests/kitchen-sink.ts b/cli/types/tests/kitchen-sink.ts index 5f5085ae8b1c..7ddd77619ae0 100644 --- a/cli/types/tests/kitchen-sink.ts +++ b/cli/types/tests/kitchen-sink.ts @@ -18,14 +18,6 @@ result // $ExpectType boolean Cypress.minimatch('/users/1/comments', '/users/*/comments') // $ExpectType boolean -// check if cy.server() yields default server options -cy.server().should((server) => { - server // $ExpectType ServerOptions - expect(server.delay).to.eq(0) - expect(server.method).to.eq('GET') - expect(server.status).to.eq(200) -}) - cy.visit('https://www.acme.com/', { auth: { username: 'wile', @@ -33,13 +25,6 @@ cy.visit('https://www.acme.com/', { } }) -const serverOptions: Partial = { - delay: 100, - ignore: () => true -} - -cy.server(serverOptions) - Cypress.spec.name // $ExpectType string Cypress.spec.relative // $ExpectType string Cypress.spec.absolute // $ExpectType string diff --git a/npm/cypress-schematic/src/e2e.spec.ts b/npm/cypress-schematic/src/e2e.spec.ts index 47bd3c434eed..e65ce18c5781 100644 --- a/npm/cypress-schematic/src/e2e.spec.ts +++ b/npm/cypress-schematic/src/e2e.spec.ts @@ -27,7 +27,7 @@ const cypressSchematicPackagePath = path.join(__dirname, '..') const ANGULAR_PROJECTS: ProjectFixtureDir[] = ['angular-13', 'angular-14'] describe('ng add @cypress/schematic / only e2e', function () { - this.timeout(1000 * 60 * 4) + this.timeout(1000 * 60 * 5) for (const project of ANGULAR_PROJECTS) { it('should install e2e files by default', async () => { diff --git a/npm/mount-utils/src/index.ts b/npm/mount-utils/src/index.ts index ac22dbfe52b5..87ace86aa7af 100644 --- a/npm/mount-utils/src/index.ts +++ b/npm/mount-utils/src/index.ts @@ -44,6 +44,18 @@ export function setupHooks (optionalCallback?: Function) { ) }) + Cypress.Commands.overwrite('session', () => { + throw new Error( + 'cy.session from a component spec is not allowed', + ) + }) + + Cypress.Commands.overwrite('origin', () => { + throw new Error( + 'cy.origin from a component spec is not allowed', + ) + }) + // @ts-ignore Cypress.on('test:before:run', () => { optionalCallback?.() diff --git a/npm/react/cypress/component/advanced/testing-lib-example/README.md b/npm/react/cypress/component/advanced/testing-lib-example/README.md index 928175a30c66..e05512916a3c 100644 --- a/npm/react/cypress/component/advanced/testing-lib-example/README.md +++ b/npm/react/cypress/component/advanced/testing-lib-example/README.md @@ -7,8 +7,7 @@ You can install and bring [Testing library/Cypress](https://testing-library.com/ ```js it('loads and displays greeting (testing-lib)', () => { - cy.server() - cy.route('/greeting', { greeting: 'Hello there' }).as('greet') + cy.intercept('/greeting', { greeting: 'Hello there' }).as('greet') const url = '/greeting' mount() @@ -19,7 +18,7 @@ it('loads and displays greeting (testing-lib)', () => { cy.findByRole('heading').should('have.text', 'Hello there') cy.findByRole('button').should('be.disabled') cy.get('@greet') - .its('url') + .its('response.url') .should('match', /\/greeting$/) }) ``` diff --git a/npm/react/cypress/component/advanced/testing-lib-example/spec.cy.jsx b/npm/react/cypress/component/advanced/testing-lib-example/spec.cy.jsx index a23403687f54..f364780e6731 100644 --- a/npm/react/cypress/component/advanced/testing-lib-example/spec.cy.jsx +++ b/npm/react/cypress/component/advanced/testing-lib-example/spec.cy.jsx @@ -3,8 +3,7 @@ import { mount } from '@cypress/react' import Fetcher from './fetcher' it('loads and displays greeting', () => { - cy.server() - cy.route('/greeting', { greeting: 'Hello there' }).as('greet') + cy.intercept('/greeting', { greeting: 'Hello there' }).as('greet') const url = '/greeting' @@ -14,6 +13,6 @@ it('loads and displays greeting', () => { cy.get('[role=heading]').should('have.text', 'Hello there') cy.get('[role=button]').should('be.disabled') cy.get('@greet') - .its('url') + .its('response.url') .should('match', /\/greeting$/) }) diff --git a/npm/react/cypress/component/advanced/testing-lib-example/testing-lib.cy.jsx b/npm/react/cypress/component/advanced/testing-lib-example/testing-lib.cy.jsx index 3460b016108d..e7541a3fe7b2 100644 --- a/npm/react/cypress/component/advanced/testing-lib-example/testing-lib.cy.jsx +++ b/npm/react/cypress/component/advanced/testing-lib-example/testing-lib.cy.jsx @@ -7,8 +7,7 @@ import Fetcher from './fetcher' // NOTE: this doesn't work because of update to the @testing/library v7. Looks like build issue with current webpack config it.skip('loads and displays greeting (testing-lib)', () => { - cy.server() - cy.route('/greeting', { greeting: 'Hello there' }).as('greet') + cy.intercept('/greeting', { greeting: 'Hello there' }).as('greet') const url = '/greeting' @@ -21,6 +20,6 @@ it.skip('loads and displays greeting (testing-lib)', () => { cy.findByRole('heading').should('have.text', 'Hello there') cy.findByRole('button').should('be.disabled') cy.get('@greet') - .its('url') + .its('response.url') .should('match', /\/greeting$/) }) diff --git a/npm/react/cypress/component/basic/network/1-users.cy.jsx b/npm/react/cypress/component/basic/network/1-users.cy.jsx index 60feb015c9c2..88d391661512 100644 --- a/npm/react/cypress/component/basic/network/1-users.cy.jsx +++ b/npm/react/cypress/component/basic/network/1-users.cy.jsx @@ -15,14 +15,8 @@ context('Users', () => { }) describe('Network State', () => { - beforeEach(() => { - cy.server() - // mount the component after defining routes in tests - // preventing race conditions where you wait on untouched routes - }) - it('can inspect real data in XHR', () => { - cy.route('/users?_limit=3').as('users') + cy.intercept('/users?_limit=3').as('users') mount() cy.wait('@users') .its('response.body') @@ -34,7 +28,7 @@ context('Users', () => { it('can display mock XHR response', () => { const users = [{ id: 1, name: 'foo' }] - cy.route('GET', '/users?_limit=3', users).as('users') + cy.intercept('GET', '/users?_limit=3', users).as('users') mount() cy.get('li') .should('have.length', 1) @@ -45,7 +39,7 @@ context('Users', () => { it('can inspect mocked XHR', () => { const users = [{ id: 1, name: 'foo' }] - cy.route('GET', '/users?_limit=3', users).as('users') + cy.intercept('GET', '/users?_limit=3', users).as('users') mount() cy.wait('@users') .its('response.body') @@ -55,10 +49,8 @@ context('Users', () => { it('can delay and wait on XHR', () => { const users = [{ id: 1, name: 'foo' }] - cy.route({ - method: 'GET', - url: '/users?_limit=3', - response: users, + cy.intercept('GET', '/users?_limit=3', { + body: users, delay: 1000, }).as('users') diff --git a/npm/react/cypress/component/basic/network/2-users-fetch.cy.jsx b/npm/react/cypress/component/basic/network/2-users-fetch.cy.jsx index a60d6add000c..86dc0541282b 100644 --- a/npm/react/cypress/component/basic/network/2-users-fetch.cy.jsx +++ b/npm/react/cypress/component/basic/network/2-users-fetch.cy.jsx @@ -13,15 +13,9 @@ describe('Users with Fetch', () => { // https://github.com/bahmutov/@cypress/react/issues/347 context('mocking', () => { - beforeEach(() => { - cy.server() - // mount the component after defining routes in tests - // preventing race conditions where you wait on untouched routes - }) - it('can inspect real data from the server', () => { // spy on the request - cy.route('/users?_limit=3').as('users') + cy.intercept('/users?_limit=3').as('users') mount() cy.wait('@users') .its('response.body') @@ -34,7 +28,7 @@ describe('Users with Fetch', () => { const users = [{ id: 1, name: 'foo' }] // stub the request - cy.route('GET', '/users?_limit=3', users).as('users') + cy.intercept('GET', '/users?_limit=3', users).as('users') mount() cy.get('li') .should('have.length', 1) @@ -45,7 +39,7 @@ describe('Users with Fetch', () => { it('can inspect mocked network reaponse', () => { const users = [{ id: 1, name: 'foo' }] - cy.route('GET', '/users?_limit=3', users).as('users') + cy.intercept('GET', '/users?_limit=3', users).as('users') mount() cy.wait('@users') .its('response.body') @@ -55,10 +49,8 @@ describe('Users with Fetch', () => { it('can delay and wait on Ajax call', () => { const users = [{ id: 1, name: 'foo' }] - cy.route({ - method: 'GET', - url: '/users?_limit=3', - response: users, + cy.intercept('GET', '/users?_limit=3', { + body: users, delay: 1000, }).as('users') diff --git a/npm/vite-dev-server/cypress.config.ts b/npm/vite-dev-server/cypress.config.ts index 9109a38747ae..335561f589b9 100644 --- a/npm/vite-dev-server/cypress.config.ts +++ b/npm/vite-dev-server/cypress.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from 'cypress' export default defineConfig({ projectId: 'ypt4pf', e2e: { - experimentalSessionAndOrigin: true, defaultCommandTimeout: 10000, // these take a bit longer b/c they're e2e open mode test async setupNodeEvents (on, config) { if (!process.env.HTTP_PROXY_TARGET_FOR_ORIGIN_REQUESTS) { diff --git a/npm/vue/cypress/component/advanced/fetch-polyfill/Users.cy.js b/npm/vue/cypress/component/advanced/fetch-polyfill/Users.cy.js index 55d4215e5610..00c943821c20 100644 --- a/npm/vue/cypress/component/advanced/fetch-polyfill/Users.cy.js +++ b/npm/vue/cypress/component/advanced/fetch-polyfill/Users.cy.js @@ -10,21 +10,18 @@ describe('Fetching users with polyfill', () => { }) it('can spy on the fetch requests', () => { - cy.server() - cy.route('/users?_limit=3').as('users') + cy.intercept('/users?_limit=3').as('users') mount(Users) cy.wait('@users') - .its('responseBody.length') + .its('response.body.length') .then((length) => { cy.get('.user').should('have.length', length) }) }) it('shows loading UI while fetch is happening', () => { - cy.server() - cy.route({ - url: '/users?_limit=3', - response: 'fixture:users', + cy.intercept('/users?_limit=3', { + fixture: 'users', delay: 1000, }) diff --git a/npm/vue/cypress/component/xhr/ajax-list.cy.js b/npm/vue/cypress/component/xhr/ajax-list.cy.js index 8d45a69a4ee7..5e83d9e0927c 100644 --- a/npm/vue/cypress/component/xhr/ajax-list.cy.js +++ b/npm/vue/cypress/component/xhr/ajax-list.cy.js @@ -9,7 +9,7 @@ describe('AjaxList', () => { context('using cy.intercept()', () => { // because this component loads data right away // we need to setup XHR intercepts BEFORE mounting it - // thus each test will first do its "cy.route" + // thus each test will first do its "cy.intercept" // then will mount the component it('loads list of posts', () => { @@ -60,62 +60,4 @@ describe('AjaxList', () => { cy.get('li').should('have.length', 1) }) }) - - context('using cy.route()', () => { - // because this component loads data right away - // we need to setup XHR intercepts BEFORE mounting it - // thus each test will first do its "cy.route" - // then will mount the component - - it('loads list of posts', () => { - mount(AjaxList) - cy.get('li').should('have.length', 3) - }) - - it('can inspect real data in XHR', () => { - cy.server() - cy.route('/users?_limit=3').as('users') - mount(AjaxList) - - cy.wait('@users').its('response.body').should('have.length', 3) - }) - - it('can display mock XHR response', () => { - cy.server() - const users = [{ id: 1, name: 'foo' }] - - cy.route('GET', '/users?_limit=3', users).as('users') - mount(AjaxList) - - cy.get('li').should('have.length', 1).first().contains('foo') - }) - - it('can inspect mocked XHR', () => { - cy.server() - const users = [{ id: 1, name: 'foo' }] - - cy.route('GET', '/users?_limit=3', users).as('users') - mount(AjaxList) - - cy.wait('@users').its('response.body').should('deep.equal', users) - }) - - it('can delay and wait on XHR', () => { - cy.server() - const users = [{ id: 1, name: 'foo' }] - - cy.route({ - method: 'GET', - url: '/users?_limit=3', - response: users, - delay: 1000, - }).as('users') - - mount(AjaxList) - - cy.get('li').should('have.length', 0) - cy.wait('@users') - cy.get('li').should('have.length', 1) - }) - }) }) diff --git a/npm/webpack-batteries-included-preprocessor/.eslintignore b/npm/webpack-batteries-included-preprocessor/.eslintignore index 824d4fc8fc31..d5857ea71fbd 100644 --- a/npm/webpack-batteries-included-preprocessor/.eslintignore +++ b/npm/webpack-batteries-included-preprocessor/.eslintignore @@ -1 +1 @@ -**/tsconfig.json \ No newline at end of file +**/tsconfig.json diff --git a/npm/webpack-preprocessor/index.ts b/npm/webpack-preprocessor/index.ts index 3b93af15b35b..c61f9bf12291 100644 --- a/npm/webpack-preprocessor/index.ts +++ b/npm/webpack-preprocessor/index.ts @@ -222,7 +222,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F }) .tap((opts) => { if (opts.devtool === false) { - // disable any overrides if we've explictly turned off sourcemaps + // disable any overrides if we've explicitly turned off sourcemaps overrideSourceMaps(false, options.typescript) return @@ -248,6 +248,7 @@ const preprocessor: WebpackPreprocessor = (options: PreprocessorOptions = {}): F // so that it's working with plain javascript webpackOptions.module.rules.unshift({ test: /\.(js|ts|jsx|tsx)$/, + exclude: /node_modules/, use: [{ loader: require.resolve('@cypress/webpack-preprocessor/dist/lib/cross-origin-callback-loader.js'), options: { diff --git a/packages/CHANGELOG.md b/packages/CHANGELOG.md new file mode 100644 index 000000000000..c4424a89cb95 --- /dev/null +++ b/packages/CHANGELOG.md @@ -0,0 +1,5 @@ +# Unreleased Cypress App Changes + +## Breaking + +- Removed the `rawJson` configuration data from `Cypress.state()`. Addressed [#23945](https://github.com/cypress-io/cypress/issues/23945). \ No newline at end of file diff --git a/packages/app/cypress/e2e/reporter_header.cy.ts b/packages/app/cypress/e2e/reporter_header.cy.ts index 679c9e29b6d7..3b6cb949486b 100644 --- a/packages/app/cypress/e2e/reporter_header.cy.ts +++ b/packages/app/cypress/e2e/reporter_header.cy.ts @@ -16,7 +16,8 @@ describe('Reporter Header', () => { cy.get('[data-selected-spec="false"]').should('have.length', '27') }) - it('filters the list of specs when searching for specs', () => { + // TODO: Reenable as part of https://github.com/cypress-io/cypress/issues/23902 + it.skip('filters the list of specs when searching for specs', () => { cy.get('body').type('f') cy.findByTestId('specs-list-panel').within(() => { @@ -28,7 +29,7 @@ describe('Reporter Header', () => { cy.get('@searchInput').clear() - cy.get('[data-cy="spec-file-item"]').should('have.length', 3) + cy.get('[data-cy="spec-file-item"]').should('have.length', 23) cy.get('@searchInput').type('asdf', { force: true }) diff --git a/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts b/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts index 0dfa58e5e188..a98045bdd543 100644 --- a/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter.command_errors.cy.ts @@ -321,107 +321,6 @@ describe('errors ui', { }) }) - it('cy.route', () => { - const verify = loadErrorSpec({ - filePath: 'errors/route.cy.js', - failCount: 9, - }) - - verify('callback assertion failure', { - column: 27, - message: `expected 'actual' to equal 'expected'`, - }) - - verify('callback exception', { - column: 12, - message: 'bar is not a function', - }) - - verify('command failure', { - column: 10, - message: 'Expected to find element: #does-not-exist, but never found it', - }) - - verify('onAbort assertion failure', { - column: 29, - codeFrameText: 'onAbort', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onAbort exception', { - column: 14, - codeFrameText: 'onAbort', - message: 'bar is not a function', - }) - - verify('onRequest assertion failure', { - column: 29, - codeFrameText: 'onRequest', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onRequest exception', { - column: 14, - codeFrameText: 'onRequest', - message: 'bar is not a function', - }) - - verify('onResponse assertion failure', { - column: 29, - codeFrameText: 'onResponse', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onResponse exception', { - column: 14, - codeFrameText: 'onResponse', - message: 'bar is not a function', - }) - }) - - it('cy.server', () => { - const verify = loadErrorSpec({ - filePath: 'errors/server.cy.js', - failCount: 6, - }) - - verify('onAbort assertion failure', { - column: 29, - codeFrameText: 'onAbort', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onAbort exception', { - column: 14, - codeFrameText: 'onAbort', - message: 'bar is not a function', - }) - - verify('onRequest assertion failure', { - column: 29, - codeFrameText: 'onRequest', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onRequest exception', { - column: 14, - codeFrameText: 'onRequest', - message: 'bar is not a function', - }) - - verify('onResponse assertion failure', { - column: 29, - codeFrameText: 'onResponse', - message: `expected 'actual' to equal 'expected'`, - }) - - verify('onResponse exception', { - column: 14, - codeFrameText: 'onResponse', - message: 'bar is not a function', - }) - }) - it('cy.readFile', () => { const verify = loadErrorSpec({ filePath: 'errors/readfile.cy.js', diff --git a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts index 99f820ffd3e0..d0377188ab24 100644 --- a/packages/app/cypress/e2e/runner/sessions.ui.cy.ts +++ b/packages/app/cypress/e2e/runner/sessions.ui.cy.ts @@ -347,13 +347,19 @@ describe('runner/cypress sessions.ui.spec', { // cy.percySnapshot() // TODO: restore when Percy CSS is fixed. See https://github.com/cypress-io/cypress/issues/23435 }) - describe('errors', () => { + describe('errors', { testIsolation: false }, () => { describe('created session', () => { before(() => { - loadSpec({ - projectName: 'session-and-origin-e2e-specs', - filePath: 'session/errors.cy.js', - failCount: 7, + cy.then(async () => { + await Cypress.action('cy:url:changed', '') + await Cypress.action('cy:visit:blank', { type: 'on' }) + }) + .then(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + failCount: 7, + }) }) }) @@ -423,11 +429,7 @@ describe('runner/cypress sessions.ui.spec', { systemTestTitle: 'validate - throws an error', errMessage: 'Something went wrong!', }, - ].forEach((opts, index) => { - if (index !== 5) { - return - } - + ].forEach((opts) => { const { testCase, systemTestTitle, errMessage } = opts it(`has test error when validate ${testCase}`, () => { @@ -544,20 +546,26 @@ describe('runner/cypress sessions.ui.spec', { describe('successfully recreated session', () => { before(() => { - loadSpec({ - projectName: 'session-and-origin-e2e-specs', - filePath: 'session/errors.cy.js', - passCount: 7, - failCount: 0, - setup () { - cy.window().then((win) => { + cy.then(async () => { + await Cypress.action('cy:url:changed', '') + await Cypress.action('cy:visit:blank', { type: 'on' }) + }) + .then(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + passCount: 7, + failCount: 0, + setup () { + cy.window().then((win) => { // @ts-ignore - return win.CYPRESS_TEST_DATA = { - restoreSessionWithValidationFailure: true, - successfullyRecreatedSession: true, - } - }) - }, + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: true, + } + }) + }, + }) }) }) @@ -593,6 +601,10 @@ describe('runner/cypress sessions.ui.spec', { errMessage: 'Something went wrong!', }, ].forEach(({ testCase, systemTestTitle, errMessage }, index) => { + if (index !== 0) { + return + } + it(`has test error when validate ${testCase}`, () => { cy.contains('.test', systemTestTitle).as('example_test') @@ -614,20 +626,26 @@ describe('runner/cypress sessions.ui.spec', { describe('failed to recreated session', () => { before(() => { - loadSpec({ - projectName: 'session-and-origin-e2e-specs', - filePath: 'session/errors.cy.js', - passCount: 0, - failCount: 7, - setup () { - cy.window().then((win) => { + cy.then(async () => { + await Cypress.action('cy:url:changed', '') + await Cypress.action('cy:visit:blank', { type: 'on' }) + }) + .then(() => { + loadSpec({ + projectName: 'session-and-origin-e2e-specs', + filePath: 'session/errors.cy.js', + passCount: 0, + failCount: 7, + setup () { + cy.window().then((win) => { // @ts-ignore - return win.CYPRESS_TEST_DATA = { - restoreSessionWithValidationFailure: true, - successfullyRecreatedSession: false, - } - }) - }, + return win.CYPRESS_TEST_DATA = { + restoreSessionWithValidationFailure: true, + successfullyRecreatedSession: false, + } + }) + }, + }) }) }) diff --git a/packages/app/cypress/e2e/settings.cy.ts b/packages/app/cypress/e2e/settings.cy.ts index c6c95c1f75c3..d286613373d2 100644 --- a/packages/app/cypress/e2e/settings.cy.ts +++ b/packages/app/cypress/e2e/settings.cy.ts @@ -202,18 +202,6 @@ describe('App: Settings', () => { }) }) - cy.get('[data-cy="experiment-experimentalSessionAndOrigin"]').within(() => { - cy.validateExternalLink({ - name: 'cy.session()', - href: 'https://on.cypress.io/session', - }) - - cy.validateExternalLink({ - name: 'cy.origin()', - href: 'https://on.cypress.io/origin', - }) - }) - cy.get('[data-cy="experiment-experimentalSourceRewriting"]').within(() => { cy.validateExternalLink({ name: '#5273', diff --git a/packages/app/cypress/e2e/specs_list_e2e.cy.ts b/packages/app/cypress/e2e/specs_list_e2e.cy.ts index f66467d8fb2c..4801f5ebf1d9 100644 --- a/packages/app/cypress/e2e/specs_list_e2e.cy.ts +++ b/packages/app/cypress/e2e/specs_list_e2e.cy.ts @@ -245,7 +245,7 @@ describe('App: Spec List (E2E)', () => { cy.findByText('No specs matched your search:').should('not.be.visible') }) - // TODO: fix flaky test https://github.com/cypress-io/cypress/issues/23305 + // TODO: FIGURE OUT WHY THIS IS NOW FAILING CONSTANTLY it.skip('saves the filter when navigating to a spec and back', function () { const targetSpecFile = 'accounts_list.spec.js' diff --git a/packages/app/cypress/e2e/support/execute-spec.ts b/packages/app/cypress/e2e/support/execute-spec.ts index 911d0bc61bbc..fe0575619fdc 100644 --- a/packages/app/cypress/e2e/support/execute-spec.ts +++ b/packages/app/cypress/e2e/support/execute-spec.ts @@ -28,7 +28,7 @@ export const waitForSpecToFinish = (expectedResults, timeout?: number) => { cy.get('.failed > .num').should('exist') // Then ensure the tests are running - cy.contains('Your tests are loading...', { timeout: timeout || 20000 }).should('not.exist') + cy.contains('Your tests are loading...', { timeout: timeout || 30000 }).should('not.exist') // Then ensure the tests have finished cy.get('[aria-label="Rerun all tests"]', { timeout: timeout || 30000 }) diff --git a/packages/app/cypress/e2e/top-nav.cy.ts b/packages/app/cypress/e2e/top-nav.cy.ts index e65a10d484f7..ebb3bc16f31f 100644 --- a/packages/app/cypress/e2e/top-nav.cy.ts +++ b/packages/app/cypress/e2e/top-nav.cy.ts @@ -195,7 +195,7 @@ describe('App Top Nav Workflows', () => { }) it('hides dropdown when version in header is clicked', () => { - cy.findByTestId('cypress-update-popover').findByRole('button', { expanded: false }).as('topNavVersionButton').click() + cy.findByTestId('cypress-update-popover').findAllByRole('button').first().as('topNavVersionButton').click() cy.get('@topNavVersionButton').should('have.attr', 'aria-expanded', 'true') @@ -541,7 +541,7 @@ describe('App Top Nav Workflows', () => { cy.findByRole('button', { name: 'Log in' }).click() }) - cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.findByRole('dialog').within(() => { cy.findByRole('button', { name: 'Log in' }).click() cy.contains('http://127.0.0.1:0000/redirect-to-auth').should('be.visible') @@ -573,7 +573,7 @@ describe('App Top Nav Workflows', () => { cy.findByRole('button', { name: 'Log in' }).click() }) - cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.findByRole('dialog').within(() => { cy.findByRole('button', { name: 'Log in' }).click() cy.contains(loginText.titleFailed).should('be.visible') @@ -623,7 +623,7 @@ describe('App Top Nav Workflows', () => { cy.findByRole('button', { name: 'Log in' }).as('loginButton').click() }) - cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.findByRole('dialog').within(() => { cy.findByRole('button', { name: 'Log in' }).click() cy.contains(loginText.titleFailed).should('be.visible') @@ -660,7 +660,7 @@ describe('App Top Nav Workflows', () => { cy.findByRole('button', { name: 'Log in' }).as('loginButton').click() }) - cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.findByRole('dialog').within(() => { cy.findByRole('button', { name: 'Log in' }).click() cy.contains(loginText.titleFailed).should('be.visible') cy.contains(loginText.bodyError).should('be.visible') diff --git a/packages/app/package.json b/packages/app/package.json index 0d83a0a21ca3..6cc8d50c3458 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -29,7 +29,7 @@ "@intlify/vite-plugin-vue-i18n": "2.4.0", "@packages/frontend-shared": "0.0.0-development", "@percy/cypress": "^3.1.0", - "@testing-library/cypress": "8.0.0", + "@testing-library/cypress": "BlueWinds/cypress-testing-library#119054b5963b0d2e064b13c5cc6fc9db32c8b7b5", "@types/faker": "5.5.8", "@urql/core": "2.4.4", "@urql/vue": "0.6.2", diff --git a/packages/app/src/components/Blank.cy.tsx b/packages/app/src/components/Blank.cy.tsx index a65e8e926264..8acc0740ca85 100644 --- a/packages/app/src/components/Blank.cy.tsx +++ b/packages/app/src/components/Blank.cy.tsx @@ -1,4 +1,4 @@ -import { initial, session, sessionLifecycle, visitFailure } from './Blank' +import { initial, testIsolationBlankPage, visitFailure } from './Blank' import { getContainerEl } from '@cypress/mount-utils' describe('initial', () => { @@ -13,28 +13,9 @@ describe('initial', () => { }) }) -describe('session', () => { +describe('testIsolationBlankPage', () => { it('works', () => { - getContainerEl()!.innerHTML = session() - - cy.get('[data-cy="cypress-logo"]') - cy.get('[data-cy="text"]').should('have.text', 'Default blank page') - cy.get('[data-cy="subtext"]').should('have.text', 'This page was cleared by navigating to about:blank.') - - cy.percySnapshot() - }) - - it('works with small viewport', () => { - cy.viewport(200, 500) - getContainerEl()!.innerHTML = session() - - cy.percySnapshot() - }) -}) - -describe('sessionLifecycle', () => { - it('works', () => { - getContainerEl()!.innerHTML = sessionLifecycle() + getContainerEl()!.innerHTML = testIsolationBlankPage() cy.get('[data-cy="cypress-logo"]') cy.get('[data-cy="text"]').should('have.text', 'Default blank page') @@ -45,7 +26,7 @@ describe('sessionLifecycle', () => { it('works with small viewport', () => { cy.viewport(200, 500) - getContainerEl()!.innerHTML = sessionLifecycle() + getContainerEl()!.innerHTML = testIsolationBlankPage() cy.percySnapshot() }) diff --git a/packages/app/src/components/Blank.jsx b/packages/app/src/components/Blank.jsx index 1acdbd80782a..f2f9f0ec5688 100644 --- a/packages/app/src/components/Blank.jsx +++ b/packages/app/src/components/Blank.jsx @@ -127,15 +127,11 @@ export const initial = () => { return blankContentsHtml() } -export const sessionLifecycle = () => { +export const testIsolationBlankPage = () => { return blankContentsHtml(blankPageHeader, `${blankPageSubtext}
All active session data (cookies, localStorage and sessionStorage) across all domains are cleared.`) } -export const session = () => { - return blankContentsHtml(blankPageHeader, blankPageSubtext) -} - export const visitFailure = (props) => { const { status, statusText, contentType } = props @@ -196,7 +192,6 @@ export const visitFailure = (props) => { export const blankContents = { initial, - session, - sessionLifecycle, + testIsolationBlankPage, visitFailure, } diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 670181a5ed3b..c26804613eb4 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -48,16 +48,12 @@ export class AutIframe { this.$iframe.remove() } - showInitialBlankContents () { + _showInitialBlankPage () { this._showContents(blankContents.initial()) } - showSessionBlankContents () { - this._showContents(blankContents.session()) - } - - showSessionLifecycleBlankContents () { - this._showContents(blankContents.sessionLifecycle()) + _showTestIsolationBlankPage () { + this._showContents(blankContents.testIsolationBlankPage()) } showVisitFailure = (props) => { @@ -126,7 +122,7 @@ export class AutIframe { this.$iframe?.removeAttr('src') } - visitBlank = ({ type }: { type?: 'session' | 'session-lifecycle' }) => { + visitBlankPage = (testIsolation?: boolean) => { return new Promise((resolve) => { if (!this.$iframe) { return @@ -135,15 +131,10 @@ export class AutIframe { this.$iframe[0].src = 'about:blank' this.$iframe.one('load', () => { - switch (type) { - case 'session': - this.showSessionBlankContents() - break - case 'session-lifecycle': - this.showSessionLifecycleBlankContents() - break - default: - this.showInitialBlankContents() + if (testIsolation) { + this._showTestIsolationBlankPage() + } else { + this._showInitialBlankPage() } resolve() diff --git a/packages/app/src/runner/event-manager-types.ts b/packages/app/src/runner/event-manager-types.ts index 3ea8efdadb94..2059c281b316 100644 --- a/packages/app/src/runner/event-manager-types.ts +++ b/packages/app/src/runner/event-manager-types.ts @@ -53,7 +53,7 @@ export type SocketToDriverMap = { } export type DriverToLocalBus = { - 'visit:blank': { type?: 'session' | 'session-lifecycle' } + 'visit:blank': { testIsolation?: boolean } 'visit:failed': { status?: string, statusText: string, contentType?: () => string } 'page:loading': boolean } diff --git a/packages/app/src/runner/index.ts b/packages/app/src/runner/index.ts index ace31aa4a8f3..2340a4671758 100644 --- a/packages/app/src/runner/index.ts +++ b/packages/app/src/runner/index.ts @@ -232,7 +232,7 @@ function runSpecCT (config, spec: SpecFile) { const specSrc = getSpecUrl(config.namespace, spec.absolute) - autIframe.showInitialBlankContents() + autIframe._showInitialBlankPage() $autIframe.prop('src', specSrc) // initialize Cypress (driver) with the AUT! @@ -290,7 +290,7 @@ function runSpecE2E (config, spec: SpecFile) { el.remove() }) - autIframe.showInitialBlankContents() + autIframe.visitBlankPage() // create Spec IFrame const specSrc = getSpecUrl(config.namespace, encodeURIComponent(spec.relative)) diff --git a/packages/app/src/runner/useEventManager.ts b/packages/app/src/runner/useEventManager.ts index 91c75397b585..0cc1e51db677 100644 --- a/packages/app/src/runner/useEventManager.ts +++ b/packages/app/src/runner/useEventManager.ts @@ -47,8 +47,8 @@ export function useEventManager () { getAutIframeModel().reattachStudio() }) - eventManager.on('visit:blank', ({ type }) => { - getAutIframeModel().visitBlank({ type }) + eventManager.on('visit:blank', ({ testIsolation }) => { + getAutIframeModel().visitBlankPage(testIsolation) }) eventManager.on('run:end', () => { diff --git a/packages/app/src/specs/SpecItem.cy.tsx b/packages/app/src/specs/SpecItem.cy.tsx index 1fa303840050..a1313a4e0bd9 100644 --- a/packages/app/src/specs/SpecItem.cy.tsx +++ b/packages/app/src/specs/SpecItem.cy.tsx @@ -22,7 +22,7 @@ describe('SpecItem', () => { const parentColor = getComputedStyle($el.parent()[0]).color const highlightedElementColor = getComputedStyle($el[0]).color - cy.wrap(highlightedElementColor).should('not.equal', parentColor) + expect(highlightedElementColor).not.to.equal(parentColor) }) }) @@ -35,7 +35,7 @@ describe('SpecItem', () => { const parentColor = getComputedStyle($el.parent()[0]).color const highlightedElementColor = getComputedStyle($el[0]).color - cy.wrap(highlightedElementColor).should('equal', parentColor) + expect(highlightedElementColor).to.equal(parentColor) }) }) diff --git a/packages/config/__snapshots__/index.spec.ts.js b/packages/config/__snapshots__/index.spec.ts.js index 1793a17d66e9..c6eb06cf4c76 100644 --- a/packages/config/__snapshots__/index.spec.ts.js +++ b/packages/config/__snapshots__/index.spec.ts.js @@ -6,6 +6,7 @@ exports['config/src/index .getBreakingKeys returns list of breaking config keys 'experimentalNetworkStubbing', 'experimentalRunEvents', 'experimentalSessionSupport', + 'experimentalSessionAndOrigin', 'experimentalShadowDomSupport', 'firefoxGcInterval', 'ignoreTestFiles', @@ -36,8 +37,8 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1 'experimentalFetchPolyfill': false, 'experimentalInteractiveRunEvents': false, 'experimentalRunAllSpecs': false, - 'experimentalSessionAndOrigin': false, 'experimentalModifyObstructiveThirdPartyCode': false, + 'experimentalOriginDependencies': false, 'experimentalSourceRewriting': false, 'experimentalSingleTabRunMode': false, 'experimentalStudio': false, @@ -70,7 +71,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1 'supportFile': 'cypress/support/e2e.{js,jsx,ts,tsx}', 'supportFolder': false, 'taskTimeout': 60000, - 'testIsolation': null, + 'testIsolation': true, 'trashAssetsBeforeRuns': true, 'userAgent': null, 'video': true, @@ -99,7 +100,6 @@ exports['config/src/index .getDefaultValues returns list of public config keys 1 'socketId': null, 'socketIoCookie': '__socket', 'socketIoRoute': '/__socket', - 'xhrRoute': '/xhrs/', } exports['config/src/index .getDefaultValues returns list of public config keys for selected testing type 1'] = { @@ -122,8 +122,8 @@ exports['config/src/index .getDefaultValues returns list of public config keys f 'experimentalFetchPolyfill': false, 'experimentalInteractiveRunEvents': false, 'experimentalRunAllSpecs': false, - 'experimentalSessionAndOrigin': false, 'experimentalModifyObstructiveThirdPartyCode': false, + 'experimentalOriginDependencies': false, 'experimentalSourceRewriting': false, 'experimentalSingleTabRunMode': false, 'experimentalStudio': false, @@ -156,7 +156,7 @@ exports['config/src/index .getDefaultValues returns list of public config keys f 'supportFile': 'cypress/support/e2e.{js,jsx,ts,tsx}', 'supportFolder': false, 'taskTimeout': 60000, - 'testIsolation': null, + 'testIsolation': true, 'trashAssetsBeforeRuns': true, 'userAgent': null, 'video': true, @@ -185,7 +185,6 @@ exports['config/src/index .getDefaultValues returns list of public config keys f 'socketId': null, 'socketIoCookie': '__socket', 'socketIoRoute': '/__socket', - 'xhrRoute': '/xhrs/', } exports['config/src/index .getPublicConfigKeys returns list of public config keys 1'] = [ @@ -204,8 +203,8 @@ exports['config/src/index .getPublicConfigKeys returns list of public config key 'experimentalFetchPolyfill', 'experimentalInteractiveRunEvents', 'experimentalRunAllSpecs', - 'experimentalSessionAndOrigin', 'experimentalModifyObstructiveThirdPartyCode', + 'experimentalOriginDependencies', 'experimentalSourceRewriting', 'experimentalSingleTabRunMode', 'experimentalStudio', diff --git a/packages/config/src/browser.ts b/packages/config/src/browser.ts index 75150ea41c97..7bf4efe1e99e 100644 --- a/packages/config/src/browser.ts +++ b/packages/config/src/browser.ts @@ -165,8 +165,6 @@ export const validate = (cfg: any, onErr: (property: ErrResult | string) => void if (validationFn && value !== defaultValues[key]) { const result = validationFn(key, value, { testingType, - // TODO: remove with experimentalSessionAndOrigin. Fixed with: https://github.com/cypress-io/cypress/issues/21471 - experimentalSessionAndOrigin: cfg.e2e?.experimentalSessionAndOrigin || cfg.experimentalSessionAndOrigin, }) if (result !== true) { diff --git a/packages/config/src/options.ts b/packages/config/src/options.ts index 07f8177cb318..763cd2be343e 100644 --- a/packages/config/src/options.ts +++ b/packages/config/src/options.ts @@ -21,6 +21,7 @@ const BREAKING_OPTION_ERROR_KEY: Readonly = [ 'EXPERIMENTAL_NETWORK_STUBBING_REMOVED', 'EXPERIMENTAL_RUN_EVENTS_REMOVED', 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED', + 'EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED', 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE', 'EXPERIMENTAL_SHADOW_DOM_REMOVED', 'FIREFOX_GC_INTERVAL_REMOVED', @@ -33,7 +34,6 @@ const BREAKING_OPTION_ERROR_KEY: Readonly = [ type ValidationOptions = { testingType: TestingType | null - experimentalSessionAndOrigin: boolean } export type BreakingOptionErrorKey = typeof BREAKING_OPTION_ERROR_KEY[number] @@ -210,17 +210,16 @@ const driverConfigOptions: Array = [ validation: validate.isBoolean, isExperimental: true, }, { - // TODO: remove with experimentalSessionAndOrigin. Fixed with: https://github.com/cypress-io/cypress/issues/21471 - name: 'experimentalSessionAndOrigin', + name: 'experimentalModifyObstructiveThirdPartyCode', defaultValue: false, validation: validate.isBoolean, isExperimental: true, + requireRestartOnChange: 'server', }, { - name: 'experimentalModifyObstructiveThirdPartyCode', + name: 'experimentalOriginDependencies', defaultValue: false, validation: validate.isBoolean, isExperimental: true, - requireRestartOnChange: 'server', }, { name: 'experimentalSourceRewriting', defaultValue: false, @@ -382,38 +381,17 @@ const driverConfigOptions: Array = [ overrideLevel: 'any', }, { name: 'testIsolation', - // TODO: https://github.com/cypress-io/cypress/issues/23093 - // When experimentalSessionAndOrigin is removed and released as GA, - // update the defaultValue from undefined to 'on' and - // update this code to remove the check/override specific to enable - // 'on' by default when experimentalSessionAndOrigin=true - defaultValue: (options: Record = {}) => { - if (options.testingType === 'component') { - return null - } - - return options?.experimentalSessionAndOrigin || options?.config?.e2e?.experimentalSessionAndOrigin ? 'on' : null - }, + defaultValue: true, validation: (key: string, value: any, opts: ValidationOptions) => { - const { testingType, experimentalSessionAndOrigin } = opts + const { testingType } = opts - if (testingType == null || testingType === 'component') { - return true - } - - if (experimentalSessionAndOrigin && testingType === 'e2e') { - return validate.isOneOf('on', 'off')(key, value) - } + let configOpts = [true, false] - if (value == null) { - return true + if (testingType === 'component') { + configOpts.pop() } - return { - key, - value, - type: 'not set unless the experimentalSessionAndOrigin flag is turned on', - } + return validate.isOneOf(...configOpts)(key, value) }, overrideLevel: 'suite', }, { @@ -569,11 +547,6 @@ const runtimeOptions: Array = [ defaultValue: pkg.version, validation: validate.isString, isInternal: true, - }, { - name: 'xhrRoute', - defaultValue: '/xhrs/', - validation: validate.isString, - isInternal: true, }, ] @@ -615,6 +588,10 @@ export const breakingOptions: Readonly = [ name: 'experimentalSessionSupport', errorKey: 'EXPERIMENTAL_SESSION_SUPPORT_REMOVED', isWarning: true, + }, { + name: 'experimentalSessionAndOrigin', + errorKey: 'EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED', + isWarning: true, }, { name: 'experimentalShadowDomSupport', errorKey: 'EXPERIMENTAL_SHADOW_DOM_REMOVED', @@ -660,11 +637,6 @@ export const breakingRootOptions: Array = [ errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E', isWarning: false, testingTypes: ['e2e'], - }, { - name: 'experimentalSessionAndOrigin', - errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG_E2E', - isWarning: false, - testingTypes: ['e2e'], }, { name: 'excludeSpecPattern', errorKey: 'CONFIG_FILE_INVALID_ROOT_CONFIG', @@ -701,6 +673,12 @@ export const breakingRootOptions: Array = [ isWarning: false, testingTypes: ['e2e'], }, + { + name: 'experimentalOriginDependencies', + errorKey: 'EXPERIMENTAL_ORIGIN_DEPENDENCIES_E2E_ONLY', + isWarning: false, + testingTypes: ['e2e'], + }, ] export const testingTypeBreakingOptions: { e2e: Array, component: Array } = { @@ -709,7 +687,6 @@ export const testingTypeBreakingOptions: { e2e: Array, component name: 'experimentalSingleTabRunMode', errorKey: 'EXPERIMENTAL_SINGLE_TAB_RUN_MODE', isWarning: false, - testingTypes: ['e2e'], }, { name: 'indexHtmlFile', @@ -723,16 +700,10 @@ export const testingTypeBreakingOptions: { e2e: Array, component errorKey: 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT', isWarning: false, }, - { - name: 'experimentalSessionAndOrigin', - errorKey: 'CONFIG_FILE_INVALID_TESTING_TYPE_CONFIG_COMPONENT', - isWarning: false, - }, { name: 'experimentalStudio', errorKey: 'EXPERIMENTAL_STUDIO_E2E_ONLY', isWarning: false, - testingTypes: ['component'], }, { name: 'testIsolation', @@ -743,7 +714,11 @@ export const testingTypeBreakingOptions: { e2e: Array, component name: 'experimentalRunAllSpecs', errorKey: 'EXPERIMENTAL_RUN_ALL_SPECS_E2E_ONLY', isWarning: false, - testingTypes: ['component'], + }, + { + name: 'experimentalOriginDependencies', + errorKey: 'EXPERIMENTAL_ORIGIN_DEPENDENCIES_E2E_ONLY', + isWarning: false, }, ], } diff --git a/packages/config/src/project/utils.ts b/packages/config/src/project/utils.ts index 8d3410850a9c..58e3676eb642 100644 --- a/packages/config/src/project/utils.ts +++ b/packages/config/src/project/utils.ts @@ -403,8 +403,6 @@ export function mergeDefaults ( const defaultsForRuntime = getDefaultValues({ ...options, - // TODO: clean this up. Fixed with: https://github.com/cypress-io/cypress/issues/21471 - experimentalSessionAndOrigin: config.experimentalSessionAndOrigin, }) _.defaultsDeep(config, defaultsForRuntime) @@ -480,20 +478,6 @@ export function mergeDefaults ( throw makeConfigError(errors.get(err, ...args)) }, testingType) - // TODO: https://github.com/cypress-io/cypress/issues/23093 - // testIsolation should equal 'on' by default when experimentalSessionAndOrigin=true - // Once experimentalSessionAndOrigin is made GA, remove this logic and update the defaultValue - // to be be 'on' - if (testingType === 'e2e' && config.experimentalSessionAndOrigin) { - if (config.rawJson.testIsolation) { - config.resolved.testIsolation.from = 'config' - } else { - config.testIsolation = 'on' - config.resolved.testIsolation.value = 'on' - config.resolved.testIsolation.from === 'default' - } - } - // We need to remove the nested propertied by testing type because it has been // flattened/compacted based on the current testing type that is selected // making the config only available with the properties that are valid, diff --git a/packages/config/src/utils.ts b/packages/config/src/utils.ts index be69b9524e92..49aa790b2f83 100644 --- a/packages/config/src/utils.ts +++ b/packages/config/src/utils.ts @@ -34,7 +34,6 @@ export function setUrls (obj: any) { proxyUrl, browserUrl: rootUrl + obj.clientRoute, reporterUrl: rootUrl + obj.reporterRoute, - xhrUrl: `${obj.namespace}${obj.xhrRoute}`, } } diff --git a/packages/config/test/index.spec.ts b/packages/config/test/index.spec.ts index 8f9cd0ebf3eb..ba80bb3f5828 100644 --- a/packages/config/test/index.spec.ts +++ b/packages/config/test/index.spec.ts @@ -195,7 +195,7 @@ describe('config/src/index', () => { const isSuiteOverride = true - configUtil.validateOverridableAtRunTime({ testIsolation: 'on' }, isSuiteOverride, errorFn) + configUtil.validateOverridableAtRunTime({ testIsolation: true }, isSuiteOverride, errorFn) expect(errorFn).to.have.callCount(0) }) @@ -205,7 +205,7 @@ describe('config/src/index', () => { const isSuiteOverride = false - configUtil.validateOverridableAtRunTime({ testIsolation: false }, isSuiteOverride, errorFn) + configUtil.validateOverridableAtRunTime({ testIsolation: 'off' }, isSuiteOverride, errorFn) expect(errorFn).to.have.callCount(1) expect(errorFn).to.have.been.calledWithMatch({ diff --git a/packages/config/test/project/utils.spec.ts b/packages/config/test/project/utils.spec.ts index ce63df2ef39a..450c96b1b2b2 100644 --- a/packages/config/test/project/utils.spec.ts +++ b/packages/config/test/project/utils.spec.ts @@ -972,6 +972,16 @@ describe('config/src/project/utils', () => { expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_SUPPORT_REMOVED') }) + it('warns if experimentalSessionAndOrigin is passed', async function () { + const warning = sinon.spy(errors, 'warning') + + await this.defaults('experimentalSessionAndOrigin', true, { + experimentalSessionAndOrigin: true, + }) + + expect(warning).to.be.calledWith('EXPERIMENTAL_SESSION_AND_ORIGIN_REMOVED') + }) + it('warns if experimentalShadowDomSupport is passed', async function () { const warning = sinon.spy(errors, 'warning') @@ -1044,8 +1054,8 @@ describe('config/src/project/utils', () => { experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalOriginDependencies: { value: false, from: 'default' }, experimentalRunAllSpecs: { value: false, from: 'default' }, - experimentalSessionAndOrigin: { value: false, from: 'default' }, experimentalSingleTabRunMode: { value: false, from: 'default' }, experimentalStudio: { value: false, from: 'default' }, experimentalSourceRewriting: { value: false, from: 'default' }, @@ -1079,7 +1089,7 @@ describe('config/src/project/utils', () => { supportFile: { value: false, from: 'config' }, supportFolder: { value: false, from: 'default' }, taskTimeout: { value: 60000, from: 'default' }, - testIsolation: { value: null, from: 'default' }, + testIsolation: { value: true, from: 'default' }, trashAssetsBeforeRuns: { value: true, from: 'default' }, userAgent: { value: null, from: 'default' }, video: { value: true, from: 'default' }, @@ -1139,8 +1149,8 @@ describe('config/src/project/utils', () => { experimentalModifyObstructiveThirdPartyCode: { value: false, from: 'default' }, experimentalFetchPolyfill: { value: false, from: 'default' }, experimentalInteractiveRunEvents: { value: false, from: 'default' }, + experimentalOriginDependencies: { value: false, from: 'default' }, experimentalRunAllSpecs: { value: false, from: 'default' }, - experimentalSessionAndOrigin: { value: false, from: 'default' }, experimentalSingleTabRunMode: { value: false, from: 'default' }, experimentalStudio: { value: false, from: 'default' }, experimentalSourceRewriting: { value: false, from: 'default' }, @@ -1196,7 +1206,7 @@ describe('config/src/project/utils', () => { supportFile: { value: false, from: 'config' }, supportFolder: { value: false, from: 'default' }, taskTimeout: { value: 60000, from: 'default' }, - testIsolation: { value: null, from: 'default' }, + testIsolation: { value: true, from: 'default' }, trashAssetsBeforeRuns: { value: true, from: 'default' }, userAgent: { value: null, from: 'default' }, video: { value: true, from: 'default' }, @@ -1212,40 +1222,14 @@ describe('config/src/project/utils', () => { }) }) - it('sets testIsolation=on by default when experimentalSessionAndOrigin=true and e2e testing', () => { - sinon.stub(utils, 'getProcessEnvVars').returns({}) - - const obj = { - projectRoot: '/foo/bar', - supportFile: false, - baseUrl: 'http://localhost:8080', - experimentalSessionAndOrigin: true, - } - - const options = { - testingType: 'e2e', - } - - const getFilesByGlob = sinon.stub().returns(['path/to/file.ts']) - - return mergeDefaults(obj, options, {}, getFilesByGlob) - .then((cfg) => { - expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') - expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) - expect(cfg.resolved).to.have.property('testIsolation') - expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'on', from: 'default' }) - }) - }) - - it('honors user config for testIsolation when experimentalSessionAndOrigin=true and e2e testing', () => { + it('honors user config for testIsolation', () => { sinon.stub(utils, 'getProcessEnvVars').returns({}) const obj = { projectRoot: '/foo/bar', supportFile: false, baseUrl: 'http://localhost:8080', - experimentalSessionAndOrigin: true, - testIsolation: 'on', + testIsolation: false, } const options = { @@ -1256,10 +1240,8 @@ describe('config/src/project/utils', () => { return mergeDefaults(obj, options, {}, getFilesByGlob) .then((cfg) => { - expect(cfg.resolved).to.have.property('experimentalSessionAndOrigin') - expect(cfg.resolved.experimentalSessionAndOrigin).to.deep.eq({ value: true, from: 'config' }) expect(cfg.resolved).to.have.property('testIsolation') - expect(cfg.resolved.testIsolation).to.deep.eq({ value: 'on', from: 'config' }) + expect(cfg.resolved.testIsolation).to.deep.eq({ value: false, from: 'config' }) }) }) }) diff --git a/packages/data-context/__snapshots__/codegen.spec.ts.js b/packages/data-context/__snapshots__/codegen.spec.ts.js index d1d32f441c89..ec8c9a31947f 100644 --- a/packages/data-context/__snapshots__/codegen.spec.ts.js +++ b/packages/data-context/__snapshots__/codegen.spec.ts.js @@ -28,7 +28,6 @@ module.exports = defineConfig({ return require('./cypress/plugins/index.js')(on, config) }, baseUrl: 'localhost:3000', - experimentalSessionAndOrigin: true, }, component: { setupNodeEvents(on, config) {}, @@ -69,7 +68,6 @@ module.exports = defineConfig({ }, retries: 2, baseUrl: 'localhost:300', - experimentalSessionAndOrigin: true, slowTestThreshold: 500, }, component: { diff --git a/packages/data-context/src/sources/HtmlDataSource.ts b/packages/data-context/src/sources/HtmlDataSource.ts index 27b1f4c90f3a..0690c6216d62 100644 --- a/packages/data-context/src/sources/HtmlDataSource.ts +++ b/packages/data-context/src/sources/HtmlDataSource.ts @@ -53,7 +53,6 @@ export class HtmlDataSource { 'testingType', 'componentTesting', 'reporterUrl', - 'xhrUrl', 'namespace', 'socketIoRoute', ] diff --git a/packages/data-context/src/sources/migration/codegen.ts b/packages/data-context/src/sources/migration/codegen.ts index 74e3b9e734a6..a3109212c13c 100644 --- a/packages/data-context/src/sources/migration/codegen.ts +++ b/packages/data-context/src/sources/migration/codegen.ts @@ -495,7 +495,6 @@ export function reduceConfig (cfg: LegacyCypressConfigJson, options: CreateConfi e2e: { ...acc.e2e, supportFile: val }, } case 'baseUrl': - case 'experimentalSessionAndOrigin': return { ...acc, e2e: { ...acc.e2e, [key]: val }, diff --git a/packages/data-context/src/sources/migration/legacyOptions.ts b/packages/data-context/src/sources/migration/legacyOptions.ts index 142bcf73c8ec..78a3146c3da3 100644 --- a/packages/data-context/src/sources/migration/legacyOptions.ts +++ b/packages/data-context/src/sources/migration/legacyOptions.ts @@ -96,11 +96,6 @@ const resolvedOptions: Array = [ defaultValue: false, isExperimental: true, canUpdateDuringTestTime: false, - }, { - name: 'experimentalSessionAndOrigin', - defaultValue: false, - isExperimental: true, - canUpdateDuringTestTime: true, }, { name: 'experimentalSourceRewriting', defaultValue: false, diff --git a/packages/data-context/test/unit/sources/migration/codegen.spec.ts b/packages/data-context/test/unit/sources/migration/codegen.spec.ts index 0b0d8e072ea7..70df15bed911 100644 --- a/packages/data-context/test/unit/sources/migration/codegen.spec.ts +++ b/packages/data-context/test/unit/sources/migration/codegen.spec.ts @@ -60,7 +60,6 @@ describe('cypress.config.js generation', () => { const config: Partial = { e2e: { baseUrl: 'localhost:3000', - experimentalSessionAndOrigin: true, }, } @@ -127,7 +126,6 @@ describe('cypress.config.js generation', () => { const config = { viewportWidth: 300, baseUrl: 'localhost:300', - experimentalSessionAndOrigin: true, slowTestThreshold: 500, e2e: { retries: 2, diff --git a/packages/driver/cross-origin-testing.md b/packages/driver/cross-origin-testing.md index 0d54267d31f2..48858eb7c3b8 100644 --- a/packages/driver/cross-origin-testing.md +++ b/packages/driver/cross-origin-testing.md @@ -6,8 +6,9 @@ The goal of this document is to give a technical overview of the architecture be See [Node.js’s URL doc](https://nodejs.org/api/url.html#url-strings-and-url-objects) for a handy breakdown of URL parts -- **domain**: A hostname without the subdomain (for the purposes of this doc). May also be referred to as **superdomain** (e.g. `example.com`, `example.co.uk`, `localhost`) -- **origin**: The combination of the protocol, hostname, and port of a URL. For the purposes of Cypress, the subdomain is irrelevant. (e.g. `http://example.com:3500`) +- **domain**: The hostname portion of a URL. (e.g. `example.com`, `www.example.com`, `www.example.co.uk`, `localhost`) +- **superdomain**: A domain without the subdomain. (e.g. `example.com`, `example.co.uk`, `localhost`) +- **origin**: The combination of the protocol, hostname, and port of a URL (e.g. `http://www.example.com:3500`) - **top**: The main window/frame of the browser - **primary origin**: The origin that top is on - **secondary origin**: Any origin that is not the primary origin @@ -209,13 +210,4 @@ Nesting **cy.origin()** inside the callback is not currently supported, but supp ### cy.intercept() -Most use-cases for **cy.intercept()** can be accomplished by using it outside of the **cy.origin()** callback. Since there may be use-cases where setting up a response, for example, requires the scope within the **cy.origin()** callback, we will likely [add support for **cy.intercept()** in the future](https://github.com/cypress-io/cypress/issues/20720). - -### Deprecated commands / methods - -All deprecated APIs are not supported in the **cy.origin()** callback and we do not plan to ever add support for them. If a user attempts to use one, we throw an error that points them to the preferred API that superseded it. The following are deprecated APIs that are not supported: - -- **cy.route()**: Superseded by **cy.intercept()** -- **cy.server()**: Superseded by **cy.intercept()** -- **Cypress.Server.defaults()**: Superseded by **cy.intercept()** -- **Cypress.Cookies.preserveOnce()**: Superseded by sessions +Most use-cases for **cy.intercept()** can be accomplished by using it outside of the **cy.origin()** callback. Since there may be use-cases where setting up a response, for example, requires the scope within the **cy.origin()** callback, we will likely [add support for **cy.intercept()** in the future](https://github.com/cypress-io/cypress/issues/20720). diff --git a/packages/driver/cypress.config.ts b/packages/driver/cypress.config.ts index 55a2741a4921..fe0315b01cef 100644 --- a/packages/driver/cypress.config.ts +++ b/packages/driver/cypress.config.ts @@ -18,6 +18,8 @@ export default defineConfig({ configFile: '../../mocha-reporter-config.json', }, e2e: { + experimentalOriginDependencies: true, + experimentalModifyObstructiveThirdPartyCode: true, setupNodeEvents: (on, config) => { return require('./cypress/plugins')(on, config) }, diff --git a/packages/driver/cypress/e2e/commands/actions/check.cy.js b/packages/driver/cypress/e2e/commands/actions/check.cy.js index 49a37886a623..ec8f7f01e918 100644 --- a/packages/driver/cypress/e2e/commands/actions/check.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/check.cy.js @@ -98,6 +98,27 @@ describe('src/cy/commands/actions/check', () => { cy.get(checkbox).check() }) + it('requeries if the DOM rerenders during actionability', () => { + cy.$$('[name=colors]').first().prop('disabled', true) + + const listener = _.after(3, () => { + cy.$$('[name=colors]').first().prop('disabled', false) + + const parent = cy.$$('[name=colors]').parent() + + parent.replaceWith(parent[0].outerHTML) + cy.off('command:retry', listener) + }) + + cy.on('command:retry', listener) + + cy.get('[name=colors]').check().then(($inputs) => { + $inputs.each((i, el) => { + expect($(el)).to.be.checked + }) + }) + }) + // readonly should only be limited to inputs, not checkboxes it('can check readonly checkboxes', () => { cy.get('#readonly-checkbox').check().then(($checkbox) => { @@ -302,24 +323,27 @@ describe('src/cy/commands/actions/check', () => { }) it('can set options.waitForAnimations', () => { - cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!')) + let retries = 0 + + cy.on('command:retry', () => { + retries += 1 + }) - cy.get(':checkbox:first').check({ waitForAnimations: false }).then(() => { - expect(cy.ensureElementIsNotAnimating).not.to.be.called + cy.get(':checkbox:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).check({ waitForAnimations: false }).then(() => { + expect(retries).to.eq(0) }) }) it('can set options.animationDistanceThreshold', () => { - const $btn = cy.$$(':checkbox:first') + let retries = 0 - cy.spy(cy, 'ensureElementIsNotAnimating') - cy.get(':checkbox:first').check({ animationDistanceThreshold: 1000 }).then(() => { - const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) - const { args } = cy.ensureElementIsNotAnimating.firstCall - - expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) + cy.on('command:retry', () => { + retries += 1 + }) - expect(args[2]).to.eq(1000) + cy.get(':checkbox:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).check({ animationDistanceThreshold: 1000 }).then(() => { + // One retry, because $actionability always waits for two sets of points to determine if an element is animating. + expect(retries).to.eq(1) }) }) @@ -437,7 +461,7 @@ describe('src/cy/commands/actions/check', () => { cy.on('fail', (err) => { expect(checked).to.eq(1) - expect(err.message).to.include('`cy.check()` failed because this element') + expect(err.message).to.include('`cy.check()` failed because the page updated') done() }) @@ -1079,7 +1103,7 @@ describe('src/cy/commands/actions/check', () => { cy.on('fail', (err) => { expect(unchecked).to.eq(1) - expect(err.message).to.include('`cy.uncheck()` failed because this element') + expect(err.message).to.include('`cy.uncheck()` failed because the page updated') done() }) diff --git a/packages/driver/cypress/e2e/commands/actions/clear.cy.js b/packages/driver/cypress/e2e/commands/actions/clear.cy.js index e751a6469a4a..09a0b6059d89 100644 --- a/packages/driver/cypress/e2e/commands/actions/clear.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/clear.cy.js @@ -52,6 +52,26 @@ describe('src/cy/commands/actions/type - #clear', () => { }) }) + it('requeries if the DOM rerenders during actionability', () => { + const clicked = cy.stub() + const retried = cy.stub() + + const textarea = cy.$$('#comments').val('foo bar').prop('disabled', true) + + cy.on('command:retry', _.after(3, () => { + if (!retried.callCount) { + textarea.replaceWith(textarea[0].outerHTML) + cy.$$('#comments').prop('disabled', false).on('click', clicked) + retried() + } + })) + + cy.get('#comments').clear().then(() => { + expect(clicked).to.be.calledOnce + expect(retried).to.be.called + }) + }) + it('can force clear even when being covered by another element', () => { const $input = $('') .attr('id', 'input-covered-in-span') @@ -275,7 +295,7 @@ describe('src/cy/commands/actions/type - #clear', () => { cy.on('fail', (err) => { expect(cleared).to.be.calledOnce - expect(err.message).to.include('`cy.clear()` failed because this element') + expect(err.message).to.include('`cy.clear()` failed because the page updated') done() }) diff --git a/packages/driver/cypress/e2e/commands/actions/click.cy.js b/packages/driver/cypress/e2e/commands/actions/click.cy.js index 2dc1f0ec0e1b..b1a9b4cb1fd7 100644 --- a/packages/driver/cypress/e2e/commands/actions/click.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/click.cy.js @@ -738,6 +738,27 @@ describe('src/cy/commands/actions/click', () => { }) }) + it('requeries if the DOM rerenders during actionability', () => { + cy.$$('[name=colors]').first().prop('disabled', true) + + const listener = _.after(3, () => { + cy.$$('[name=colors]').first().prop('disabled', false) + + const parent = cy.$$('[name=colors]').parent() + + parent.replaceWith(parent[0].outerHTML) + cy.off('command:retry', listener) + }) + + cy.on('command:retry', listener) + + cy.get('[name=colors]').first().click().then(($inputs) => { + $inputs.each((i, el) => { + expect($(el)).to.be.checked + }) + }) + }) + it('increases the timeout delta after each click', () => { const count = cy.$$('#three-buttons button').length @@ -813,22 +834,20 @@ describe('src/cy/commands/actions/click', () => { }) it('places cursor at the end of [contenteditable]', () => { - cy.get('[contenteditable]:first') - .invoke('html', '

').click() - .then(expectCaret(0)) + cy.get('[contenteditable]:first').as('edit') + + cy.get('@edit').invoke('html', '

') + cy.get('@edit').click().then(expectCaret(0)) - cy.get('[contenteditable]:first') - .invoke('html', 'foo').click() - .then(expectCaret(3)) + cy.get('@edit').invoke('html', 'foo') + cy.get('@edit').click().then(expectCaret(3)) - cy.get('[contenteditable]:first') - .invoke('html', '
foo
').click() - .then(expectCaret(3)) + cy.get('@edit').invoke('html', '
foo
') + cy.get('@edit').click().then(expectCaret(3)) - cy.get('[contenteditable]:first') // firefox headless: prevent contenteditable from disappearing (dont set to empty) - .invoke('html', '
').click() - .then(expectCaret(0)) + cy.get('@edit').invoke('html', '
') + cy.get('@edit').click().then(expectCaret(0)) }) it('can click SVG elements', () => { @@ -1034,6 +1053,15 @@ describe('src/cy/commands/actions/click', () => { }) describe('actionability', () => { + let retries = 0 + + beforeEach(() => { + retries = 0 + cy.on('command:retry', () => { + retries += 1 + }) + }) + it('can click on inline elements that wrap lines', () => { cy.get('#overflow-link').find('.wrapped').click() }) @@ -1320,24 +1348,19 @@ describe('src/cy/commands/actions/click', () => { $('span on button').css({ position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block', backgroundColor: 'yellow' }).prependTo(cy.$$('body')) const scrolled = [] - let retried = false let clicked = false cy.on('scrolled', ($el, type) => { scrolled.push(type) }) - cy.on('command:retry', () => { - retried = true - }) - $btn.on('click', () => { clicked = true }) cy.get('#button-covered-in-span').click({ force: true }).then(() => { expect(scrolled).to.be.empty - expect(retried).to.be.false + expect(retries).to.eq(0) expect(clicked).to.be.true }) @@ -1348,19 +1371,14 @@ describe('src/cy/commands/actions/click', () => { $('span on button').css({ opacity: 0, position: 'absolute', left: $btn.offset().left, top: $btn.offset().top, padding: 5, display: 'inline-block' }).prependTo(cy.$$('body')) - let retried = false let clicked = false - cy.on('command:retry', () => { - retried = true - }) - $btn.on('click', () => { clicked = true }) cy.get('#button-covered-in-span').click({ force: true }).then(() => { - expect(retried).to.be.false + expect(retries).to.be.eq(0) expect(clicked).to.be.true }) }) @@ -1380,7 +1398,6 @@ describe('src/cy/commands/actions/click', () => { }).prependTo(cy.$$('body')) const scrolled = [] - let retried = false cy.on('scrolled', ($el, type) => { scrolled.push(type) @@ -1388,11 +1405,10 @@ describe('src/cy/commands/actions/click', () => { cy.on('command:retry', _.after(3, () => { $span.hide() - retried = true })) cy.get('#button-covered-in-span').click().then(() => { - expect(retried).to.be.true + expect(retries).to.be.gt(0) // - element scrollIntoView // - element scrollIntoView (retry animation coords) @@ -1553,22 +1569,18 @@ describe('src/cy/commands/actions/click', () => { it('waits until element becomes visible', () => { const $btn = cy.$$('#button').hide() - let retried = false - cy.on('command:retry', _.after(3, () => { $btn.show() - retried = true })) cy.get('#button').click().then(() => { - expect(retried).to.be.true + expect(retries).to.be.gt(0) }) }) it('waits until element is no longer disabled', () => { const $btn = cy.$$('#button').prop('disabled', true) - let retried = false let clicks = 0 $btn.on('click', () => { @@ -1577,83 +1589,64 @@ describe('src/cy/commands/actions/click', () => { cy.on('command:retry', _.after(3, () => { $btn.prop('disabled', false) - retried = true })) cy.get('#button').click().then(() => { expect(clicks).to.eq(1) - expect(retried).to.be.true + expect(retries).to.be.gt(0) }) }) - it('waits until element stops animating', () => { - let retries = 0 - - cy.on('command:retry', () => { - retries += 1 - }) + it('succeeds when DOM rerenders and returns new subject', () => { + const $btn = cy.$$('#button').prop('disabled', true) - cy.stub(cy, 'ensureElementIsNotAnimating') - .throws(new Error('animating!')) - .onThirdCall().returns() + cy.on('command:retry', _.after(3, () => { + $btn.replaceWith('') + })) - cy.get('button:first').click().then(() => { - // - retry animation coords - // - retry animation - // - retry animation - expect(retries).to.eq(3) + cy.get('#button').click().should('contain', 'New Button') + }) - expect(cy.ensureElementIsNotAnimating).to.be.calledThrice + it('waits until element stops animating', () => { + cy.get('button:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).click().then(() => { + expect(retries).to.gt(1) }) }) it('does not throw when waiting for animations is disabled', { waitForAnimations: false, }, () => { - cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!')) - - cy.get('button:first').click().then(() => { - expect(cy.ensureElementIsNotAnimating).not.to.be.called + cy.get('button:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).click().then(() => { + expect(retries).to.eq(0) }) }) it('does not throw when turning off waitForAnimations in options', () => { - cy.stub(cy, 'ensureElementIsNotAnimating').throws(new Error('animating!')) - - cy.get('button:first').click({ waitForAnimations: false }).then(() => { - expect(cy.ensureElementIsNotAnimating).not.to.be.called + cy.get('button:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).click({ waitForAnimations: false }).then(() => { + expect(retries).to.eql(0) }) }) - it('passes options.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { - const $btn = cy.$$('button:first') - - cy.spy(cy, 'ensureElementIsNotAnimating') - cy.get('button:first').click({ animationDistanceThreshold: 1000 }).then(() => { - const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) - const { args } = cy.ensureElementIsNotAnimating.firstCall - - expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) - - expect(args[2]).to.eq(1000) + it('passes options.animationDistanceThreshold to $actionability.ensureElIsNotAnimating', () => { + cy.get('button:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).click({ animationDistanceThreshold: 1000 }).then(() => { + // One retry, because $actionability waits for two sets of position coordinates. + expect(retries).to.eq(1) }) }) - it('passes config.animationDistanceThreshold to cy.ensureElementIsNotAnimating', () => { - const animationDistanceThreshold = Cypress.config('animationDistanceThreshold') - - const $btn = cy.$$('button:first') - - cy.spy(cy, 'ensureElementIsNotAnimating') - - cy.get('button:first').click().then(() => { - const { fromElWindow } = Cypress.dom.getElementCoordinatesByPosition($btn) - const { args } = cy.ensureElementIsNotAnimating.firstCall + it('passes config.animationDistanceThreshold to $actionability.ensureElIsNotAnimating', () => { + let old = Cypress.config('animationDistanceThreshold') - expect(args[1]).to.deep.eq([fromElWindow, fromElWindow]) + Cypress.config('animationDistanceThreshold', 1000) - expect(args[2]).to.eq(animationDistanceThreshold) + cy.get('button:first').then(($btn) => $btn.animate({ width: '30em' }, 100)).click().then(() => { + try { + // One retry, because $actionability waits for two sets of position coordinates. + expect(retries).to.eq(1) + } finally { + Cypress.config('animationDistanceThreshold', old) + } }) }) @@ -2110,41 +2103,21 @@ describe('src/cy/commands/actions/click', () => { cy.get('.badge-multi').click() }) - it('throws when subject is not in the document', (done) => { - let clicked = 0 - - const $checkbox = cy.$$(':checkbox:first').click(() => { - clicked += 1 - $checkbox.remove() - - return false - }) - - cy.on('fail', (err) => { - expect(clicked).to.eq(1) - expect(err.message).to.include('`cy.click()` failed because this element is detached from the DOM') - - done() - }) - - cy.get(':checkbox:first').click().click() - }) + // This is an instance of an unfixable detached DOM error: .then() is a command, so it sets the subject to a + // *specific element*, which then gets detached. + // The error message tells the user exactly how to fix this case. it('throws when subject is detached during actionability', (done) => { cy.on('fail', (err) => { - expect(err.message).to.include('`cy.click()` failed because this element is detached from the DOM') + expect(err.message).to.include('`cy.click()` failed because the page updated while this command was executing.') + expect(err.message).to.include('You can typically solve this by breaking up a chain.') done() }) cy.get('input:first') .then(($el) => { - // This represents an asynchronous re-render - // since we fire the 'scrolled' event during actionability - // if we use el.on('scroll'), headless electron is flaky - cy.on('scrolled', () => { - $el.remove() - }) + cy.on('scrolled', () => $el.remove()) }) .click() }) @@ -3259,26 +3232,6 @@ describe('src/cy/commands/actions/click', () => { cy.dblclick() }) - it('throws when subject is not in the document', (done) => { - let dblclicked = 0 - - const $button = cy.$$('button:first').dblclick(() => { - dblclicked += 1 - $button.remove() - - return false - }) - - cy.on('fail', (err) => { - expect(dblclicked).to.eq(1) - expect(err.message).to.include('`cy.dblclick()` failed because this element') - - done() - }) - - cy.get('button:first').dblclick().dblclick() - }) - it('logs once when not dom subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this @@ -3350,21 +3303,6 @@ describe('src/cy/commands/actions/click', () => { }) }) - // TODO: remove this after 4.0 when {multiple:true} is no longer default - // https://github.com/cypress-io/cypress/issues/5406 - it('does not log default option {multiple:true}', () => { - const logs = [] - - cy.on('log:added', (attrs, log) => { - logs.push(log) - }) - - cy.get('button:first').dblclick().then(() => { - expect(logs[1].get('message')).to.eq('') - expect(logs[1].invoke('consoleProps').Options).not.ok - }) - }) - it('returns only the $el for the element of the subject that was dblclicked', () => { const dblclicks = [] @@ -3698,26 +3636,6 @@ describe('src/cy/commands/actions/click', () => { cy.rightclick() }) - it('throws when subject is not in the document', (done) => { - let rightclicked = 0 - - const $button = cy.$$('button:first').on('contextmenu', () => { - rightclicked += 1 - $button.remove() - - return false - }) - - cy.on('fail', (err) => { - expect(rightclicked).to.eq(1) - expect(err.message).to.include('`cy.rightclick()` failed because this element') - - done() - }) - - cy.get('button:first').rightclick().rightclick() - }) - it('logs once when not dom subject', function (done) { cy.on('fail', (err) => { const { lastLog } = this diff --git a/packages/driver/cypress/e2e/commands/actions/focus.cy.js b/packages/driver/cypress/e2e/commands/actions/focus.cy.js index 1a86f1048c45..b2a48f488cbc 100644 --- a/packages/driver/cypress/e2e/commands/actions/focus.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/focus.cy.js @@ -336,7 +336,7 @@ describe('src/cy/commands/actions/focus', () => { cy.on('fail', (err) => { expect(focused).to.eq(1) - expect(err.message).to.include('`cy.focus()` failed because this element') + expect(err.message).to.include('`cy.focus()` failed because the page updated') done() }) @@ -791,7 +791,7 @@ describe('src/cy/commands/actions/focus', () => { cy.on('fail', (err) => { expect(blurred).to.eq(1) - expect(err.message).to.include('`cy.blur()` failed because this element') + expect(err.message).to.include('`cy.blur()` failed because the page') expect(err.docsUrl).to.include('https://on.cypress.io/element-has-detached-from-dom') done() diff --git a/packages/driver/cypress/e2e/commands/actions/scroll.cy.js b/packages/driver/cypress/e2e/commands/actions/scroll.cy.js index cd6e456be732..055a71b2625d 100644 --- a/packages/driver/cypress/e2e/commands/actions/scroll.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/scroll.cy.js @@ -323,7 +323,7 @@ describe('src/cy/commands/actions/scroll', () => { }) it('retries until element is scrollable', () => { - const $container = cy.$$('#nonscroll-becomes-scrollable') + let $container = cy.$$('#nonscroll-becomes-scrollable') expect($container.get(0).scrollTop).to.eq(0) expect($container.get(0).scrollLeft).to.eq(0) @@ -331,6 +331,11 @@ describe('src/cy/commands/actions/scroll', () => { let retried = false cy.on('command:retry', _.after(2, () => { + // Replacing the element with itself to ensure that .scrollTo() is requerying the DOM + // as necessary + $container.replaceWith($container[0].outerHTML) + $container = cy.$$('#nonscroll-becomes-scrollable') + $container.css('overflow', 'scroll') retried = true })) @@ -347,10 +352,10 @@ describe('src/cy/commands/actions/scroll', () => { const scrollTo = cy.spy($.fn, 'scrollTo') cy.get('button:first').scrollTo('bottom', { ensureScrollable: false }).then(() => { - cy.stub(cy, 'ensureScrollability') + cy.stub(Cypress.ensure, 'isScrollable') expect(scrollTo).to.be.calledWithMatch({}, { ensureScrollable: false }) - expect(cy.ensureScrollability).not.to.be.called + expect(Cypress.ensure.isScrollable).not.to.be.called }) }) }) @@ -385,17 +390,17 @@ describe('src/cy/commands/actions/scroll', () => { }) it('waits until the subject is scrollable', () => { - cy.stub(cy, 'ensureScrollability') + cy.stub(Cypress.ensure, 'isScrollable') .onFirstCall().throws(new Error()) cy.on('command:retry', () => { - return cy.ensureScrollability.returns() + return Cypress.ensure.isScrollable.returns() }) cy .get('#scroll-into-view-horizontal') .scrollTo('right').then(() => { - expect(cy.ensureScrollability).to.be.calledTwice + expect(Cypress.ensure.isScrollable).to.be.calledTwice }) }) }) @@ -430,7 +435,7 @@ describe('src/cy/commands/actions/scroll', () => { context('subject errors', () => { it('throws when not passed DOM element as subject', (done) => { cy.on('fail', (err) => { - expect(err.message).to.include('`cy.scrollTo()` failed because it requires a DOM element.') + expect(err.message).to.include('`cy.scrollTo()` failed because it requires a DOM element or window.') expect(err.message).to.include('{foo: bar}') expect(err.message).to.include('> `cy.noop()`') @@ -450,6 +455,17 @@ describe('src/cy/commands/actions/scroll', () => { cy.get('button').scrollTo('500px') }) + + it('throws if subject disappears while waiting for scrollability', (done) => { + cy.on('command:retry', () => cy.$$('#nonscroll-becomes-scrollable').remove()) + + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.scrollTo()` failed because the page updated') + done() + }) + + cy.get('#nonscroll-becomes-scrollable').scrollTo(500, 300) + }) }) context('argument errors', () => { diff --git a/packages/driver/cypress/e2e/commands/actions/select.cy.js b/packages/driver/cypress/e2e/commands/actions/select.cy.js index f0ed01b70247..e62a3ffdc871 100644 --- a/packages/driver/cypress/e2e/commands/actions/select.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/select.cy.js @@ -214,11 +214,13 @@ describe('src/cy/commands/actions/select', () => { const select = cy.$$('select[name=disabled]') cy.on('command:retry', _.once(() => { - select.prop('disabled', false) + // Replace the element with a copy of itself, to ensure .select() is requerying the DOM + select.replaceWith(select[0].outerHTML) + cy.$$('select[name=disabled]').prop('disabled', false) })) cy.get('select[name=disabled]').select('foo') - .invoke('val').should('eq', 'foo') + cy.get('select[name=disabled]').invoke('val').should('eq', 'foo') }) it('retries until is no longer disabled', () => { @@ -376,7 +378,7 @@ describe('src/cy/commands/actions/select', () => { cy.on('fail', (err) => { expect(selected).to.eq(1) - expect(err.message).to.include('`cy.select()` failed because this element') + expect(err.message).to.include('`cy.select()` failed because the page updated') done() }) @@ -543,8 +545,7 @@ describe('src/cy/commands/actions/select', () => { it('throws when the is disabled by a disabled
', (done) => { cy.on('fail', (err) => { - expect(err.message).to.include('`cy.select()` failed because this element is currently disabled:') - expect(err.docsUrl).to.eq('https://on.cypress.io/select') + expect(err.message).to.include('`cy.select()` failed because this element is `disabled`:') done() }) @@ -648,7 +648,7 @@ describe('src/cy/commands/actions/select', () => { cy.get('#select-maps').select('de_dust2').then(function ($select) { const { lastLog } = this - expect(lastLog.get('$el')).to.eq($select) + expect(lastLog.get('$el')).to.eql($select) }) }) diff --git a/packages/driver/cypress/e2e/commands/actions/selectFile.cy.js b/packages/driver/cypress/e2e/commands/actions/selectFile.cy.js index e75eb986615f..aeff3963e635 100644 --- a/packages/driver/cypress/e2e/commands/actions/selectFile.cy.js +++ b/packages/driver/cypress/e2e/commands/actions/selectFile.cy.js @@ -336,7 +336,7 @@ describe('src/cy/commands/actions/selectFile', () => { }) describe('errors', { - defaultCommandTimeout: 500, + defaultCommandTimeout: 100, }, () => { it('is a child command', (done) => { cy.on('fail', (err) => { @@ -358,17 +358,17 @@ describe('src/cy/commands/actions/selectFile', () => { it('throws when non-input subject', function (done) { cy.on('fail', (err) => { - expect(err.message).to.include('`cy.selectFile()` can only be called on an `` or a `