diff --git a/.circleci/config.yml b/.circleci/config.yml index 5c6ae226c0ff..94a5a54a0b37 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -27,7 +27,7 @@ mainBuildFilters: &mainBuildFilters branches: only: - develop - - 'feature/v8-snapshots' + - 'release/11.0.0' # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -36,7 +36,7 @@ macWorkflowFilters: &darwin-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'feature/v8-snapshots', << pipeline.git.branch >> ] + - equal: [ 'release/11.0.0', << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -45,7 +45,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'feature/v8-snapshots', << pipeline.git.branch >> ] + - equal: [ 'release/11.0.0', << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -63,7 +63,7 @@ windowsWorkflowFilters: &windows-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'feature/v8-snapshots', << pipeline.git.branch >> ] + - equal: [ 'release/11.0.0', << pipeline.git.branch >> ] - matches: pattern: "-release$" value: << pipeline.git.branch >> @@ -130,7 +130,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "feature/v8-snapshots" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/11.0.0" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/.gitignore b/.gitignore index 7fb44280b48d..ce0f3b177ba0 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,9 @@ cli/visual-snapshots # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +# Fleet +.fleet/ + # User-specific stuff .idea .idea/**/workspace.xml diff --git a/cli/lib/cli.js b/cli/lib/cli.js index 052893ca4c99..1cbc8547fb5f 100644 --- a/cli/lib/cli.js +++ b/cli/lib/cli.js @@ -405,7 +405,21 @@ module.exports = { args = process.argv } - const { CYPRESS_INTERNAL_ENV } = process.env + const { CYPRESS_INTERNAL_ENV, CYPRESS_DOWNLOAD_USE_CA } = process.env + + if (process.env.CYPRESS_DOWNLOAD_USE_CA) { + let msg = ` + ${logSymbols.warning} Warning: It looks like you're setting CYPRESS_DOWNLOAD_USE_CA=${CYPRESS_DOWNLOAD_USE_CA} + + The environment variable "CYPRESS_DOWNLOAD_USE_CA" is no longer required to be set. + + You can safely unset this environment variable. + ` + + logger.log() + logger.warn(stripIndent(msg)) + logger.log() + } if (!util.isValidCypressInternalEnvValue(CYPRESS_INTERNAL_ENV)) { debug('invalid CYPRESS_INTERNAL_ENV value', CYPRESS_INTERNAL_ENV) diff --git a/cli/lib/tasks/download.js b/cli/lib/tasks/download.js index 6b8cd38b69b2..17b6c733f301 100644 --- a/cli/lib/tasks/download.js +++ b/cli/lib/tasks/download.js @@ -40,13 +40,7 @@ const getBaseUrl = () => { const getCA = () => { return new Promise((resolve) => { - if (!util.getEnv('CYPRESS_DOWNLOAD_USE_CA')) { - resolve() - } - - if (process.env.npm_config_ca) { - resolve(process.env.npm_config_ca) - } else if (process.env.npm_config_cafile) { + if (process.env.npm_config_cafile) { fs.readFile(process.env.npm_config_cafile, 'utf8') .then((cafileContent) => { resolve(cafileContent) @@ -54,6 +48,8 @@ const getCA = () => { .catch(() => { resolve() }) + } else if (process.env.npm_config_ca) { + resolve(process.env.npm_config_ca) } else { resolve() } diff --git a/cli/test/lib/tasks/download_spec.js b/cli/test/lib/tasks/download_spec.js index 13c1176795b4..7617cdcd4ebe 100644 --- a/cli/test/lib/tasks/download_spec.js +++ b/cli/test/lib/tasks/download_spec.js @@ -607,7 +607,7 @@ describe('lib/tasks/download', function () { // prevent ambient environment masking of environment variables referenced in this test ;([ - 'CYPRESS_DOWNLOAD_USE_CA', 'NO_PROXY', 'http_proxy', + 'NO_PROXY', 'http_proxy', 'https_proxy', 'npm_config_ca', 'npm_config_cafile', 'npm_config_https_proxy', 'npm_config_proxy', ]).forEach((e) => { @@ -683,7 +683,6 @@ describe('lib/tasks/download', function () { }) it('returns CA from npm_config_ca', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_ca = 'foo' return download.getCA().then((ca) => { @@ -692,7 +691,6 @@ describe('lib/tasks/download', function () { }) it('returns CA from npm_config_cafile', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_cafile = 'test/fixture/cafile.pem' return download.getCA().then((ca) => { @@ -701,7 +699,6 @@ describe('lib/tasks/download', function () { }) it('returns undefined if failed reading npm_config_cafile', () => { - process.env.CYPRESS_DOWNLOAD_USE_CA = 'true' process.env.npm_config_cafile = 'test/fixture/not-exists.pem' return download.getCA().then((ca) => { diff --git a/npm/angular/CHANGELOG.md b/npm/angular/CHANGELOG.md index 497d78066094..9d914ff00da5 100644 --- a/npm/angular/CHANGELOG.md +++ b/npm/angular/CHANGELOG.md @@ -1,3 +1,16 @@ +# [@cypress/angular-v2.0.0](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.1.2...@cypress/angular-v2.0.0) (2022-11-07) + + +### Bug Fixes + +* possibility to override global services in Angular component tests ([#24394](https://github.com/cypress-io/cypress/issues/24394)) ([54d2853](https://github.com/cypress-io/cypress/commit/54d285321723450920e0f1d50374c4bd0590e72a)) +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + # [@cypress/angular-v1.1.2](https://github.com/cypress-io/cypress/compare/@cypress/angular-v1.1.1...@cypress/angular-v1.1.2) (2022-10-11) diff --git a/npm/angular/src/mount.ts b/npm/angular/src/mount.ts index 7af3a98ed5e0..d45757ab721d 100644 --- a/npm/angular/src/mount.ts +++ b/npm/angular/src/mount.ts @@ -8,7 +8,7 @@ window.Mocha['__zone_patch__'] = false import 'zone.js/testing' import { CommonModule } from '@angular/common' -import { Component, ErrorHandler, EventEmitter, Injectable, SimpleChange, SimpleChanges, Type } from '@angular/core' +import { Component, ErrorHandler, EventEmitter, Injectable, SimpleChange, SimpleChanges, Type, OnChanges } from '@angular/core' import { ComponentFixture, getTestBed, @@ -72,6 +72,23 @@ export interface MountConfig extends TestModuleMetadata { componentProperties?: Partial<{ [P in keyof T]: T[P] }> } +let activeFixture: ComponentFixture | null = null + +function cleanup () { + // Not public, we need to call this to remove the last component from the DOM + try { + (getTestBed() as any).tearDownTestingModule() + } catch (e) { + const notSupportedError = new Error(`Failed to teardown component. The version of Angular you are using may not be officially supported.`) + + ;(notSupportedError as any).docsUrl = 'https://on.cypress.io/component-framework-configuration' + throw notSupportedError + } + + getTestBed().resetTestingModule() + activeFixture = null +} + /** * Type that the `mount` function returns * @type MountResponse @@ -163,22 +180,12 @@ function initTestBed ( component: Type | string, config: MountConfig, ): Type { - const { providers, ...configRest } = config - const componentFixture = createComponentFixture(component) as Type getTestBed().configureTestingModule({ - ...bootstrapModule(componentFixture, configRest), + ...bootstrapModule(componentFixture, config), }) - if (providers != null) { - getTestBed().overrideComponent(componentFixture, { - add: { - providers, - }, - }) - } - return componentFixture } @@ -219,6 +226,8 @@ function setupFixture ( ): ComponentFixture { const fixture = getTestBed().createComponent(component) + setupComponent(config, fixture) + fixture.whenStable().then(() => { fixture.autoDetectChanges(config.autoDetectChanges ?? true) }) @@ -233,17 +242,18 @@ function setupFixture ( * @param {ComponentFixture} fixture Fixture for debugging and testing a component. * @returns {T} Component being mounted */ -function setupComponent ( +function setupComponent ( config: MountConfig, - fixture: ComponentFixture): T { - let component: T = fixture.componentInstance + fixture: ComponentFixture, +): void { + let component = fixture.componentInstance as unknown as { [key: string]: any } & Partial if (config?.componentProperties) { component = Object.assign(component, config.componentProperties) } if (config.autoSpyOutputs) { - Object.keys(component).forEach((key: string, index: number, keys: string[]) => { + Object.keys(component).forEach((key) => { const property = component[key] if (property instanceof EventEmitter) { @@ -262,56 +272,61 @@ function setupComponent 0) { component.ngOnChanges(simpleChanges) } } - - return component } /** * Mounts an Angular component inside Cypress browser * - * @param {Type | string} component Angular component being mounted or its template - * @param {MountConfig} config configuration used to configure the TestBed + * @param component Angular component being mounted or its template + * @param config configuration used to configure the TestBed * @example - * import { HelloWorldComponent } from 'hello-world/hello-world.component' + * import { mount } from '@cypress/angular' + * import { StepperComponent } from './stepper.component' * import { MyService } from 'services/my.service' * import { SharedModule } from 'shared/shared.module'; - * import { mount } from '@cypress/angular' - * it('can mount', () => { - * mount(HelloWorldComponent, { - * providers: [MyService], - * imports: [SharedModule] - * }) - * cy.get('h1').contains('Hello World') + * it('mounts', () => { + * mount(StepperComponent, { + * providers: [MyService], + * imports: [SharedModule] + * }) + * cy.get('[data-cy=increment]').click() + * cy.get('[data-cy=counter]').should('have.text', '1') * }) * - * or + * // or * - * it('can mount with template', () => { - * mount('', { - * declarations: [HelloWorldComponent], - * providers: [MyService], - * imports: [SharedModule] - * }) + * it('mounts with template', () => { + * mount('', { + * declarations: [StepperComponent], + * }) * }) - * @returns Cypress.Chainable> + * + * @see {@link https://on.cypress.io/mounting-angular} for more details. + * + * @returns A component and component fixture */ export function mount ( component: Type | string, config: MountConfig = { }, ): Cypress.Chainable> { + // Remove last mounted component if cy.mount is called more than once in a test + if (activeFixture) { + cleanup() + } + const componentFixture = initTestBed(component, config) - const fixture = setupFixture(componentFixture, config) - const componentInstance = setupComponent(config, fixture) + + activeFixture = setupFixture(componentFixture, config) const mountResponse: MountResponse = { - fixture, - component: componentInstance, + fixture: activeFixture, + component: activeFixture.componentInstance, } const logMessage = typeof component === 'string' ? 'Component' : componentFixture.name @@ -330,6 +345,15 @@ export function mount ( * * @param {string} alias name you want to use for your cy.spy() alias * @returns EventEmitter + * @example + * import { StepperComponent } from './stepper.component' + * import { mount, createOutputSpy } from '@cypress/angular' + * + * it('Has spy', () => { + * mount(StepperComponent, { change: createOutputSpy('changeSpy') }) + * cy.get('[data-cy=increment]').click() + * cy.get('@changeSpy').should('have.been.called') + * }) */ export const createOutputSpy = (alias: string) => { const emitter = new EventEmitter() @@ -348,8 +372,4 @@ getTestBed().initTestEnvironment( }, ) -setupHooks(() => { - // Not public, we need to call this to remove the last component from the DOM - getTestBed()['tearDownTestingModule']() - getTestBed().resetTestingModule() -}) +setupHooks(cleanup) diff --git a/npm/angular/tsconfig.json b/npm/angular/tsconfig.json index b21ac64dacea..a73e01dcecc9 100644 --- a/npm/angular/tsconfig.json +++ b/npm/angular/tsconfig.json @@ -11,15 +11,15 @@ "allowJs": true, "declaration": true, "outDir": "dist", - "strict": false, - "noImplicitAny": false, + "strict": true, "baseUrl": "./", "types": [ "cypress" ], "allowSyntheticDefaultImports": true, "esModuleInterop": true, - "moduleResolution": "node" + "moduleResolution": "node", + "noPropertyAccessFromIndexSignature": true, }, "include": ["src/**/*.*"], "exclude": ["src/**/*-spec.*"] diff --git a/npm/mount-utils/CHANGELOG.md b/npm/mount-utils/CHANGELOG.md index 4dab067c7bb4..97153754cebd 100644 --- a/npm/mount-utils/CHANGELOG.md +++ b/npm/mount-utils/CHANGELOG.md @@ -1,3 +1,17 @@ +# [@cypress/mount-utils-v3.0.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v2.1.0...@cypress/mount-utils-v3.0.0) (2022-11-07) + + +### Bug Fixes + +* remove dependence on @cypress/ types ([#24415](https://github.com/cypress-io/cypress/issues/24415)) ([58e0ab9](https://github.com/cypress-io/cypress/commit/58e0ab91604618ea6f75932622f7e66e419270e6)) +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) +* remove some CT functions and props ([#24419](https://github.com/cypress-io/cypress/issues/24419)) ([294985f](https://github.com/cypress-io/cypress/commit/294985f8b3e0fa00ed66d25f88c8814603766074)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + # [@cypress/mount-utils-v2.1.0](https://github.com/cypress-io/cypress/compare/@cypress/mount-utils-v2.0.1...@cypress/mount-utils-v2.1.0) (2022-08-30) diff --git a/npm/mount-utils/README.md b/npm/mount-utils/README.md index 014e4f3052d4..b789cf73af8b 100644 --- a/npm/mount-utils/README.md +++ b/npm/mount-utils/README.md @@ -25,11 +25,7 @@ All the functionality used to create the first party Mount adapters is available In addition, we recommend that Mount Adapters: -- receive a second argument that extends `StyleOptions` from `@cypress/mount-utils` -- calls `injectStylesBeforeElement` from `@cypress/mount-utils` before mounting the component -- calls `setupHooks` to register the required lifecycle hooks for `@cypress/mount-utils` to work - -This will let the user inject styles `` and stylesheets ``, which is very useful for developing components. +- call `setupHooks` to register the required lifecycle hooks for `@cypress/mount-utils` to work ### Example Mount Adapter: Web Components @@ -39,9 +35,7 @@ Here's a simple yet realistic example of Mount Adapter targeting Web Components. import { ROOT_SELECTOR, setupHooks, - injectStylesBeforeElement, - getContainerEl, - StyleOptions + getContainerEl } from "@cypress/mount-utils"; Cypress.on("run:start", () => { @@ -69,8 +63,7 @@ function maybeRegisterComponent( } export function mount( - webComponent: CustomElementConstructor, - options?: Partial + webComponent: CustomElementConstructor ): Cypress.Chainable { // Get root selector defined in `cypress/support.component-index.html const $root = document.querySelector(ROOT_SELECTOR)!; @@ -83,9 +76,6 @@ export function mount( /// Register Web Component maybeRegisterComponent(name, webComponent); - // Inject user styles before mounting the component - injectStylesBeforeElement(options ?? {}, document, getContainerEl()) - // Render HTML containing component. $root.innerHTML = `<${name} id="root">`; @@ -100,8 +90,7 @@ export function mount( return cy.wrap(document.querySelector("#root"), { log: false }); } -// Setup Cypress lifecycle hooks. This tears down any styles -// injected by injectStylesBeforeElement, etc. +// Setup Cypress lifecycle hooks. setupHooks(); ``` @@ -131,14 +120,7 @@ export class WebCounter extends HTMLElement { describe('web-component.cy.ts', () => { it('playground', () => { - cy.mount(WebCounter, { - styles: ` - button { - background: lightblue; - color: white; - } - ` - }) + cy.mount(WebCounter) }) }) ``` diff --git a/npm/mount-utils/create-rollup-entry.mjs b/npm/mount-utils/create-rollup-entry.mjs index 2db5e4781262..f3b0de4d610c 100644 --- a/npm/mount-utils/create-rollup-entry.mjs +++ b/npm/mount-utils/create-rollup-entry.mjs @@ -4,6 +4,7 @@ import resolve from '@rollup/plugin-node-resolve' import commonjs from '@rollup/plugin-commonjs' import _ from 'lodash' import { readFileSync } from 'fs' +import dts from 'rollup-plugin-dts' const pkg = JSON.parse(readFileSync('./package.json')) @@ -33,7 +34,7 @@ export function createEntries (options) { check: format === 'es', tsconfigOverride: { compilerOptions: { - declaration: format === 'es', + declaration: false, target: 'es6', module: format === 'cjs' ? 'es2015' : 'esnext', }, @@ -67,5 +68,19 @@ export function createEntries (options) { console.log(`Building ${format}: ${finalConfig.output.file}`) return finalConfig - }) + }).concat([{ + input, + output: [{ file: 'dist/index.d.ts', format: 'es' }], + plugins: [ + dts({ respectExternal: true }), + { + name: 'cypress-types-reference', + // rollup-plugin-dts does not add '// ' like rollup-plugin-typescript2 did so we add it here. + renderChunk (...[code]) { + return `/// \n\n${code}` + }, + }, + ], + external: config.external || [], + }]) } diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json index b171f80da79c..8ab99841eecb 100644 --- a/npm/mount-utils/package.json +++ b/npm/mount-utils/package.json @@ -15,6 +15,7 @@ "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-node-resolve": "^11.1.1", "rollup": "^2.38.5", + "rollup-plugin-dts": "^4.2.3", "rollup-plugin-typescript2": "^0.29.0", "typescript": "^4.7.4" }, diff --git a/npm/mount-utils/src/index.ts b/npm/mount-utils/src/index.ts index 5593110bd505..ac22dbfe52b5 100644 --- a/npm/mount-utils/src/index.ts +++ b/npm/mount-utils/src/index.ts @@ -1,44 +1,10 @@ -/** - * Additional styles to inject into the document. - * A component might need 3rd party libraries from CDN, - * local CSS files and custom styles. - */ -export interface StyleOptions { - /** - * Creates element for each stylesheet - * @alias stylesheet - */ - stylesheets: string | string[] - /** - * Creates element for each stylesheet - * @alias stylesheets - */ - stylesheet: string | string[] - /** - * Creates element and inserts given CSS. - * @alias styles - */ - style: string | string[] - /** - * Creates element for each given CSS text. - * @alias style - */ - styles: string | string[] - /** - * Loads each file and creates a element - * with the loaded CSS - * @alias cssFile - */ - cssFiles: string | string[] - /** - * Single CSS file to load into a element - * @alias cssFile - */ - cssFile: string | string[] -} - export const ROOT_SELECTOR = '[data-cy-root]' +/** + * Gets the root element used to mount the component. + * @returns {HTMLElement} The root element + * @throws {Error} If the root element is not found + */ export const getContainerEl = (): HTMLElement => { const el = document.querySelector(ROOT_SELECTOR) @@ -49,159 +15,20 @@ export const getContainerEl = (): HTMLElement => { throw Error(`No element found that matches selector ${ROOT_SELECTOR}. Please add a root element with data-cy-root attribute to your "component-index.html" file so that Cypress can attach your component to the DOM.`) } -/** - * Remove any style or extra link elements from the iframe placeholder - * left from any previous test - * - */ -export function cleanupStyles () { - const styles = document.body.querySelectorAll('[data-cy=injected-style-tag]') - - styles.forEach((styleElement) => { - if (styleElement.parentElement) { - styleElement.parentElement.removeChild(styleElement) - } - }) - - const links = document.body.querySelectorAll('[data-cy=injected-stylesheet]') - - links.forEach((link) => { - if (link.parentElement) { - link.parentElement.removeChild(link) +export function checkForRemovedStyleOptions (mountingOptions: Record) { + for (const key of ['cssFile', 'cssFiles', 'style', 'styles', 'stylesheet', 'stylesheets'] as const) { + if (mountingOptions[key]) { + Cypress.utils.throwErrByPath('mount.removed_style_mounting_options', key) } - }) -} - -/** - * Insert links to external style resources. - */ -function insertStylesheets ( - stylesheets: string[], - document: Document, - el: HTMLElement | null, -) { - stylesheets.forEach((href) => { - const link = document.createElement('link') - - link.type = 'text/css' - link.rel = 'stylesheet' - link.href = href - link.dataset.cy = 'injected-stylesheet' - document.body.insertBefore(link, el) - }) -} - -/** - * Inserts a single stylesheet element - */ -function insertStyles (styles: string[], document: Document, el: HTMLElement | null) { - styles.forEach((style) => { - const styleElement = document.createElement('style') - - styleElement.dataset.cy = 'injected-style-tag' - styleElement.appendChild(document.createTextNode(style)) - document.body.insertBefore(styleElement, el) - }) -} - -function insertSingleCssFile ( - cssFilename: string, - document: Document, - el: HTMLElement | null, - log?: boolean, -) { - return cy.readFile(cssFilename, { log }).then((css) => { - const style = document.createElement('style') - - style.appendChild(document.createTextNode(css)) - document.body.insertBefore(style, el) - }) -} - -/** - * Reads the given CSS file from local file system - * and adds the loaded style text as an element. - */ -function insertLocalCssFiles ( - cssFilenames: string[], - document: Document, - el: HTMLElement | null, - log?: boolean, -) { - return Cypress.Promise.mapSeries(cssFilenames, (cssFilename) => { - return insertSingleCssFile(cssFilename, document, el, log) - }) + } } /** - * Injects custom style text or CSS file or 3rd party style resources - * into the given document. + * Utility function to register CT side effects and run cleanup code during the "test:before:run" Cypress hook + * @param optionalCallback Callback to be called before the next test runs */ -export const injectStylesBeforeElement = ( - options: Partial, - document: Document, - el: HTMLElement | null, -): HTMLElement => { - if (!el) return - - // first insert all stylesheets as Link elements - let stylesheets: string[] = [] - - if (typeof options.stylesheet === 'string') { - stylesheets.push(options.stylesheet) - } else if (Array.isArray(options.stylesheet)) { - stylesheets = stylesheets.concat(options.stylesheet) - } - - if (typeof options.stylesheets === 'string') { - options.stylesheets = [options.stylesheets] - } - - if (options.stylesheets) { - stylesheets = stylesheets.concat(options.stylesheets) - } - - insertStylesheets(stylesheets, document, el) - - // insert any styles as elements - let styles: string[] = [] - - if (typeof options.style === 'string') { - styles.push(options.style) - } else if (Array.isArray(options.style)) { - styles = styles.concat(options.style) - } - - if (typeof options.styles === 'string') { - styles.push(options.styles) - } else if (Array.isArray(options.styles)) { - styles = styles.concat(options.styles) - } - - insertStyles(styles, document, el) - - // now load any css files by path and add their content - // as elements - let cssFiles: string[] = [] - - if (typeof options.cssFile === 'string') { - cssFiles.push(options.cssFile) - } else if (Array.isArray(options.cssFile)) { - cssFiles = cssFiles.concat(options.cssFile) - } - - if (typeof options.cssFiles === 'string') { - cssFiles.push(options.cssFiles) - } else if (Array.isArray(options.cssFiles)) { - cssFiles = cssFiles.concat(options.cssFiles) - } - - return insertLocalCssFiles(cssFiles, document, el, options.log) -} - export function setupHooks (optionalCallback?: Function) { - // Consumed by the framework "mount" libs. A user might register their own mount in the scaffolded 'commands.js' - // file that is imported by e2e and component support files by default. We don't want CT side effects to run when e2e + // We don't want CT side effects to run when e2e // testing so we early return. // System test to verify CT side effects do not pollute e2e: system-tests/test/e2e_with_mount_import_spec.ts if (Cypress.testingType !== 'component') { @@ -220,6 +47,41 @@ export function setupHooks (optionalCallback?: Function) { // @ts-ignore Cypress.on('test:before:run', () => { optionalCallback?.() - cleanupStyles() }) } + +/** + * Remove any style or extra link elements from the iframe placeholder + * left from any previous test + * + * Removed as of Cypress 11.0.0 + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates + */ +export function cleanupStyles () { + Cypress.utils.throwErrByPath('mount.cleanup_styles') +} + +/** + * Additional styles to inject into the document. + * A component might need 3rd party libraries from CDN, + * local CSS files and custom styles. + * + * Removed as of Cypress 11.0.0. + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates + */ +export type StyleOptions = unknown + +/** + * Injects custom style text or CSS file or 3rd party style resources + * into the given document. + * + * Removed as of Cypress 11.0.0. + * @see https://on.cypress.io/migration-11-0-0-component-testing-updates + */ +export const injectStylesBeforeElement = ( + options: Partial, + document: Document, + el: HTMLElement | null, +) => { + Cypress.utils.throwErrByPath('mount.inject_styles_before_element') +} diff --git a/npm/react/CHANGELOG.md b/npm/react/CHANGELOG.md index 817d0c090e39..6fcd6b7632fb 100644 --- a/npm/react/CHANGELOG.md +++ b/npm/react/CHANGELOG.md @@ -1,3 +1,16 @@ +# [@cypress/react-v7.0.0](https://github.com/cypress-io/cypress/compare/@cypress/react-v6.2.1...@cypress/react-v7.0.0) (2022-11-07) + + +### Bug Fixes + +* remove last mounted component upon subsequent mount calls ([#24470](https://github.com/cypress-io/cypress/issues/24470)) ([f39eb1c](https://github.com/cypress-io/cypress/commit/f39eb1c19e0923bda7ae263168fc6448da942d54)) +* remove some CT functions and props ([#24419](https://github.com/cypress-io/cypress/issues/24419)) ([294985f](https://github.com/cypress-io/cypress/commit/294985f8b3e0fa00ed66d25f88c8814603766074)) + + +### BREAKING CHANGES + +* remove last mounted component upon subsequent mount calls of mount + # [@cypress/react-v6.2.1](https://github.com/cypress-io/cypress/compare/@cypress/react-v6.2.0...@cypress/react-v6.2.1) (2022-11-01) diff --git a/npm/react/README.md b/npm/react/README.md index 23582443e447..cc2793fedd9b 100644 --- a/npm/react/README.md +++ b/npm/react/README.md @@ -31,8 +31,6 @@ For more information, please check the official docs for [running Cypress](https - `mount` is the most important function, allows to mount a given React component as a mini web application and interact with it using Cypress commands - `createMount` factory function that creates new `mount` function with default options -- `unmount` removes previously mounted component, mostly useful to test how the component cleans up after itself -- `mountHook` mounts a given React Hook in a test component for full testing, see `hooks` example ## Examples @@ -65,20 +63,7 @@ it('looks right', () => { }) ``` -### Extra styles - -You can pass additional styles, css files and external stylesheets to load, see [docs/styles.md](./docs/styles.md) for the full list of options. - -```js -const todo = { - id: '123', - title: 'Write more tests', -} -mount(, { - stylesheets: [ - 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.css', - ], -}) +See [docs/styles.md](./docs/styles.md) for full list of options. ``` You may also specify the `ReactDOM` package to use. This can be useful in complex monorepo setups that have different versions of React and React DOM installed. If you see an error relating to [mismatching versions of React or React DOM](https://reactjs.org/warnings/invalid-hook-call-warning.html#mismatching-versions-of-react-and-react-dom), this may be the solution. You can do this using the `ReactDom` option: @@ -87,12 +72,7 @@ You may also specify the `ReactDOM` package to use. This can be useful in comple // if you have multiple versions of ReactDom in your monorepo import ReactDom from 'react-dom' -mount(, { - stylesheets: [ - 'https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.css', - ], - ReactDom -}) +mount(, { reactDom: ReactDom }) ``` ## Compatibility diff --git a/npm/react/cypress/component/advanced/hooks/README.md b/npm/react/cypress/component/advanced/hooks/README.md index 053f08b3336f..4d849faa2d34 100644 --- a/npm/react/cypress/component/advanced/hooks/README.md +++ b/npm/react/cypress/component/advanced/hooks/README.md @@ -1,8 +1,3 @@ # testing React hooks -- [counter-with-hooks.spec.js](counter-with-hooks.spec.js) and [counter2-with-hooks.spec.js](counter2-with-hooks.spec.js) test React components that uses hooks -- [use-counter.spec.js](use-counter.spec.js) shows how to test a React hook using `mountHook` function - -![Hook test](images/hook.png) - -Note: hooks are mounted inside a test component following the approach shown in [react-hooks-testing-library](https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js) +- [counter-with-hooks.spec.js](counter-with-hooks.spec.js) and [counter2-with-hooks.spec.js](counter2-with-hooks.spec.js) test React components that uses hooks \ No newline at end of file diff --git a/npm/react/cypress/component/advanced/hooks/images/hook.png b/npm/react/cypress/component/advanced/hooks/images/hook.png deleted file mode 100644 index e9d9077e60d5..000000000000 Binary files a/npm/react/cypress/component/advanced/hooks/images/hook.png and /dev/null differ diff --git a/npm/react/cypress/component/advanced/hooks/use-counter.cy.jsx b/npm/react/cypress/component/advanced/hooks/use-counter.cy.jsx deleted file mode 100644 index 1586b396eb21..000000000000 --- a/npm/react/cypress/component/advanced/hooks/use-counter.cy.jsx +++ /dev/null @@ -1,25 +0,0 @@ -// @ts-check -/// -import { useState, useCallback } from 'react' -import { mountHook } from '@cypress/react' - -// testing example hook function from -// https://dev.to/jooforja/12-recipes-for-testing-react-applications-using-testing-library-1bh2#hooks -function useCounter () { - const [count, setCount] = useState(0) - const increment = useCallback(() => setCount((x) => x + 1), []) - - return { count, increment } -} - -describe('useCounter hook', function () { - it('increments the count', function () { - mountHook(() => useCounter()).then((result) => { - expect(result.current.count).to.equal(0) - result.current.increment() - expect(result.current.count).to.equal(1) - result.current.increment() - expect(result.current.count).to.equal(2) - }) - }) -}) diff --git a/npm/react/cypress/component/advanced/renderless/mouse.cy.jsx b/npm/react/cypress/component/advanced/renderless/mouse.cy.jsx index 027317cdb169..670d3dc92214 100644 --- a/npm/react/cypress/component/advanced/renderless/mouse.cy.jsx +++ b/npm/react/cypress/component/advanced/renderless/mouse.cy.jsx @@ -1,6 +1,6 @@ /// import React from 'react' -import { mount, unmount } from '@cypress/react' +import { mount } from '@cypress/react' import MouseMovement from './mouse-movement' describe('Renderless component', () => { @@ -23,7 +23,8 @@ describe('Renderless component', () => { expect(onMoved).to.have.been.calledWith(true) }) - unmount() + // mount something else to trigger unmount + mount(
Test Component
) cy.get('@log') .its('callCount') diff --git a/npm/react/cypress/component/advanced/set-timeout-example/loading-indicator.cy.jsx b/npm/react/cypress/component/advanced/set-timeout-example/loading-indicator.cy.jsx index a790d84f7319..3157851f9f59 100644 --- a/npm/react/cypress/component/advanced/set-timeout-example/loading-indicator.cy.jsx +++ b/npm/react/cypress/component/advanced/set-timeout-example/loading-indicator.cy.jsx @@ -1,5 +1,5 @@ import React from 'react' -import { mount, unmount } from '@cypress/react' +import { mount } from '@cypress/react' import LoadingIndicator from './LoadingIndicator' // compare these tests to Jest + Enzyme tests in @@ -73,7 +73,9 @@ describe('LoadingIndicator', () => { ) cy.tick(2010) - unmount() + + // mount something else to trigger unmount + mount(
Test Component
) cy.get('@clearTimeout').should('have.been.calledOnce') }) diff --git a/npm/react/cypress/component/advanced/timers/card-without-effect.cy.jsx b/npm/react/cypress/component/advanced/timers/card-without-effect.cy.jsx index 32831f17b102..64f8a075d13b 100644 --- a/npm/react/cypress/component/advanced/timers/card-without-effect.cy.jsx +++ b/npm/react/cypress/component/advanced/timers/card-without-effect.cy.jsx @@ -1,7 +1,7 @@ /// import Card from './card-without-effect.jsx' import React from 'react' -import { mount, unmount } from '@cypress/react' +import { mount } from '@cypress/react' it('should select null after timing out', () => { const onSelect = cy.stub() @@ -28,7 +28,8 @@ it('should cleanup on being removed', () => { expect(onSelect).to.not.have.been.called }) - unmount() + // mount something else so that unmount is called + mount(
Test Component
) cy.tick(5000).then(() => { expect(onSelect).to.not.have.been.called @@ -44,7 +45,8 @@ it('should cleanup on being removed (using unmount)', () => { expect(onSelect).to.not.have.been.called }) - unmount() + // mount something else so that unmount is called + mount(
Test Component
) cy.tick(5000).then(() => { expect(onSelect).to.not.have.been.called diff --git a/npm/react/cypress/component/advanced/tutorial/square.cy.jsx b/npm/react/cypress/component/advanced/tutorial/square.cy.jsx index cc34619cae32..33e70df52aaf 100644 --- a/npm/react/cypress/component/advanced/tutorial/square.cy.jsx +++ b/npm/react/cypress/component/advanced/tutorial/square.cy.jsx @@ -1,6 +1,7 @@ /// import React from 'react' import { mount } from '@cypress/react' +import './tic-tac-toe.css' // let's put React component right in the spec file class Square extends React.Component { @@ -37,16 +38,13 @@ describe('Square', () => { }) it('looks good', () => { - mount(, { - cssFile: 'cypress/component/advanced/tutorial/tic-tac-toe.css', - }) + mount() // pause to show it cy.wait(1000) cy.get('.square').click() cy.wait(1000) - // check if style was applied cy.get('.square') .should('have.css', 'background-color', 'rgb(255, 255, 255)') .and('have.css', 'border', '1px solid rgb(153, 153, 153)') diff --git a/npm/react/cypress/component/advanced/tutorial/tic-tac-toe.cy.jsx b/npm/react/cypress/component/advanced/tutorial/tic-tac-toe.cy.jsx index 79c2b6456d19..c4b93f89803c 100644 --- a/npm/react/cypress/component/advanced/tutorial/tic-tac-toe.cy.jsx +++ b/npm/react/cypress/component/advanced/tutorial/tic-tac-toe.cy.jsx @@ -4,6 +4,7 @@ import React from 'react' import { mount } from '@cypress/react' import { Game } from './tic-tac-toe.jsx' +import './tic-tac-toe.css' describe('Tic Tac Toe', () => { /** @@ -18,9 +19,7 @@ describe('Tic Tac Toe', () => { }) it('starts and lets X win', () => { - mount(, { - cssFile: 'cypress/component/advanced/tutorial/tic-tac-toe.css', - }) + mount() cy.contains('.status', 'Next player: X') clickSquare(0, 0).click() diff --git a/npm/react/cypress/component/basic/alias/README.md b/npm/react/cypress/component/basic/alias/README.md deleted file mode 100644 index b0977d132596..000000000000 --- a/npm/react/cypress/component/basic/alias/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# alias - -You can retrieve the created component using default constructor function name or an alias. See [alias-spec.js](alias-spec.js) for examples. - -![Alias tests](images/alias.png) diff --git a/npm/react/cypress/component/basic/alias/alias.cy.jsx b/npm/react/cypress/component/basic/alias/alias.cy.jsx deleted file mode 100644 index 8aa2a38145ae..000000000000 --- a/npm/react/cypress/component/basic/alias/alias.cy.jsx +++ /dev/null @@ -1,31 +0,0 @@ -/// -import React from 'react' -import { mount } from '@cypress/react' - -describe('Alias', () => { - it('returns component by its name', () => { - const Greeting = () =>
Hello!
- - mount() - // get the component instance by name "Greeting" - cy.get('@Greeting') - .its('props') - .should('be.empty') - - // the component was constructed from the function Greeting - cy.get('@Greeting') - .its('type') - .should('equal', Greeting) - }) - - it('returns component by given display name', () => { - const GreetingCard = (props) =>
Hello {props.name}!
- - mount(, { alias: 'Hello' }) - cy.get('@Hello') - .its('props') - .should('deep.equal', { - name: 'World', - }) - }) -}) diff --git a/npm/react/cypress/component/basic/alias/images/alias.png b/npm/react/cypress/component/basic/alias/images/alias.png deleted file mode 100644 index 71aa416890e4..000000000000 Binary files a/npm/react/cypress/component/basic/alias/images/alias.png and /dev/null differ diff --git a/npm/react/cypress/component/basic/enzyme/props.cy.jsx b/npm/react/cypress/component/basic/enzyme/props.cy.jsx index 693d5ca90d31..0ccb58844c06 100644 --- a/npm/react/cypress/component/basic/enzyme/props.cy.jsx +++ b/npm/react/cypress/component/basic/enzyme/props.cy.jsx @@ -36,10 +36,11 @@ describe('Enzyme', () => { context('setProps', () => { it('gets props from the component', () => { - mount() + mount().as('Foo') cy.contains('initial').should('be.visible') cy.get('@Foo') + .its('component') .its('props') .then((props) => { console.log('current props', props) diff --git a/npm/react/cypress/component/basic/hello-world.cy.jsx b/npm/react/cypress/component/basic/hello-world.cy.jsx index 38eb4913b373..7593fd71e05e 100644 --- a/npm/react/cypress/component/basic/hello-world.cy.jsx +++ b/npm/react/cypress/component/basic/hello-world.cy.jsx @@ -1,5 +1,4 @@ /// -/// import React from 'react' import { mount } from '@cypress/react' import { HelloWorld } from './hello-world.jsx' @@ -10,4 +9,10 @@ describe('HelloWorld component', () => { mount() cy.contains('Hello World!') }) + + it('errors if passing alias', () => { + expect(() => mount(, { alias: 'foo' })).to.throw( + `passing \`alias\` to mounting options is no longer supported. Use mount(...).as('foo') instead.`, + ) + }) }) diff --git a/npm/react/cypress/component/basic/rerender/effects.cy.jsx b/npm/react/cypress/component/basic/rerender/effects.cy.jsx index b27550dbc13e..6b804dc0d81e 100644 --- a/npm/react/cypress/component/basic/rerender/effects.cy.jsx +++ b/npm/react/cypress/component/basic/rerender/effects.cy.jsx @@ -1,6 +1,7 @@ /// import React, { useLayoutEffect, useEffect } from 'react' -import { mount } from '@cypress/react' +import ReactDom from 'react-dom' +import { mount, getContainerEl } from '@cypress/react' it('should not run unmount effect cleanup when rerendering', () => { const layoutEffectCleanup = cy.stub() @@ -50,7 +51,7 @@ it('should run unmount effect cleanup when unmounting', () => { return
{input}
} - mount().then(({ rerender, unmount }) => { + mount().then(({ rerender }) => { expect(layoutEffectCleanup).to.have.been.callCount(0) expect(effectCleanup).to.have.been.callCount(0) @@ -59,7 +60,9 @@ it('should run unmount effect cleanup when unmounting', () => { expect(effectCleanup).to.have.been.callCount(0) }) - unmount().then(() => { + cy + .then(() => ReactDom.unmountComponentAtNode(getContainerEl())) + .then(() => { expect(layoutEffectCleanup).to.have.been.callCount(1) expect(effectCleanup).to.have.been.callCount(1) }) diff --git a/npm/react/cypress/component/basic/styles/README.md b/npm/react/cypress/component/basic/styles/README.md deleted file mode 100644 index 28dcae470bb8..000000000000 --- a/npm/react/cypress/component/basic/styles/README.md +++ /dev/null @@ -1,11 +0,0 @@ -With `@cypress/react`, you can add styles to your component in three ways: - -```js -mount(, { - style: `CSS styles`, - stylesheets: '/path/to/component.css', - cssFile: '/path/to/css/file' -}) -``` - -See examples: [css-file](./css-file), [style](./style), [stylesheets](./stylesheets) diff --git a/npm/react/cypress/component/basic/styles/style/style.cy.jsx b/npm/react/cypress/component/basic/styles/style/style.cy.jsx deleted file mode 100644 index cc30f64cbf17..000000000000 --- a/npm/react/cypress/component/basic/styles/style/style.cy.jsx +++ /dev/null @@ -1,95 +0,0 @@ -/// -import React from 'react' -import { mount } from '@cypress/react' - -describe('style', () => { - const backgroundColor = 'rgb(0, 255, 0)' - const indexButtonHeight = '20px' - const buttonHeightOverride = '50px' - const className = `green` - const indexStyle = `.${className} { background-color: ${backgroundColor}; height: ${indexButtonHeight}; }` - const baseStyle = `.${className}.${className} { height: ${buttonHeightOverride} !important; }` - - context('options.style', () => { - it('string', () => { - const Component = () => - - mount(, { - style: indexStyle, - }) - - cy.get('button') - .should('have.class', className) - .and('have.css', 'background-color', backgroundColor) - }) - - it('string[]', () => { - const Component = () => - - mount(, { - style: [indexStyle], - }) - - cy.get('button') - .should('have.class', className) - .and('have.css', 'background-color', backgroundColor) - }) - }) - - context('options.styles', () => { - it('string', () => { - const Component = () => - - mount(, { - styles: indexStyle, - log: false, - }) - - cy.get('button') - .should('have.class', className) - .and('have.css', 'background-color', backgroundColor) - }) - - it('sets several', () => { - const Component = () => { - return ( - - ) - } - - mount(, { - styles: [baseStyle, indexStyle], - }) - - // check the style from the first css file - cy.get('button') - .should('have.class', className) - .invoke('css', 'height') - .should((value) => { - // round the height, since in real browser it is never exactly 50 - expect(parseFloat(value), `height is ${buttonHeightOverride}`).to.be.closeTo(50, 1) - }) - - // and should have style from the second css file - cy.get('button').and('have.css', 'background-color', backgroundColor) - }) - - it('resets the style', () => { - const Component = () => { - return ( - - ) - } - - mount() - // the component should NOT have CSS styles - - cy.get('button') - .should('have.class', className) - .invoke('css', 'height') - .should((value) => { - expect(parseFloat(value), `height is < ${indexButtonHeight}`).to.be.lessThan(30) - }) - }) - }) -}) diff --git a/npm/react/cypress/component/basic/unmount/comp.cy.jsx b/npm/react/cypress/component/basic/unmount/comp.cy.jsx index 1807a183b672..f12b80c56e6c 100644 --- a/npm/react/cypress/component/basic/unmount/comp.cy.jsx +++ b/npm/react/cypress/component/basic/unmount/comp.cy.jsx @@ -15,9 +15,15 @@ it('calls callbacks on mount and unmount', () => { }) cy.contains('Component with').should('be.visible') - unmount().then(() => { - expect(onUnmount).to.have.been.calledOnce - }) - cy.contains('Component with').should('not.exist') + let stub = cy.stub() + + try { + unmount() + } catch (e) { + expect(e.message).to.eq('`unmount` is no longer supported.') + stub() + } + + expect(stub).to.have.been.calledOnce }) diff --git a/npm/react/cypress/component/basic/unmount/unmount.cy.jsx b/npm/react/cypress/component/basic/unmount/unmount.cy.jsx index e092d95a5e98..e4bd608e723e 100644 --- a/npm/react/cypress/component/basic/unmount/unmount.cy.jsx +++ b/npm/react/cypress/component/basic/unmount/unmount.cy.jsx @@ -1,6 +1,8 @@ /// import React, { Component } from 'react' -import { mount, unmount } from '@cypress/react' +import { getContainerEl } from '@cypress/mount-utils' +import ReactDom from 'react-dom' +import { mount } from '@cypress/react' class Comp extends Component { componentWillUnmount () { @@ -20,7 +22,7 @@ describe('Comp with componentWillUnmount', () => { // after we have confirmed the component exists let's remove it // unmount() command is automatically enqueued - unmount() + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) // the component is gone from the DOM cy.contains('My component').should('not.exist') @@ -33,7 +35,7 @@ describe('Comp with componentWillUnmount', () => { cy.contains('My component') // still works, should probably be removed in v5 - cy.then(unmount) + cy.then(() => ReactDom.unmountComponentAtNode(getContainerEl())) // the component is gone from the DOM cy.contains('My component').should('not.exist') diff --git a/npm/react/cypress/component/removedMountingOptions.cy.jsx b/npm/react/cypress/component/removedMountingOptions.cy.jsx new file mode 100644 index 000000000000..7fad6402b035 --- /dev/null +++ b/npm/react/cypress/component/removedMountingOptions.cy.jsx @@ -0,0 +1,30 @@ +import React from 'react' +import { mount } from '@cypress/react' + +describe('removed mounting options', () => { + function Foo () { + return (
foo
) + } + + it('throws error when receiving removed mounting options', () => { + for (const key of ['cssFile', 'cssFiles', 'style', 'styles', 'stylesheet', 'stylesheets']) { + expect(() => mount(, { + [key]: `body { background: red; }`, + })).to.throw( + `The \`${key}\` mounting option is no longer supported.`, + ) + } + }) + + it('throws with custom command', () => { + Cypress.on('fail', (e) => { + expect(e.message).to.contain('The `styles` mounting option is no longer supported.') + + return false + }) + + cy.mount(, { + styles: 'body { background: red; }', + }) + }) +}) diff --git a/npm/react/cypress/support/component.jsx b/npm/react/cypress/support/component.jsx index 6d117868267b..e9d230785210 100644 --- a/npm/react/cypress/support/component.jsx +++ b/npm/react/cypress/support/component.jsx @@ -12,3 +12,6 @@ // You can read more here: // https://on.cypress.io/configuration // *********************************************************** +import { mount } from '@cypress/react' + +Cypress.Commands.add('mount', mount) diff --git a/npm/react/docs/styles.md b/npm/react/docs/styles.md index e4e5ebea2f5b..118e02510e64 100644 --- a/npm/react/docs/styles.md +++ b/npm/react/docs/styles.md @@ -1,6 +1,6 @@ # styles -If you component imports its own style, the style should be applied during the Cypress test. +If your component imports its own style, the style should be applied during the Cypress test. ```js // Footer.jsx @@ -20,7 +20,7 @@ it('is stylish', () => { }) ``` -## Import from spec +## Import in the spec file Sometimes the root component imports the style, or it is included from `src/public/index.html` file. We can usually import the style directly from the spec file: @@ -41,6 +41,25 @@ it('is stylish', () => { }) ``` +## Import in the component support file + +If you have stylesheets that should apply to all of your components, you can import those in your component support file. + +```js +// cypress/support/component.js +import './main.css' +... + +// Footer.spec.js +import React from 'react' +import { mount } from '@cypress/react' +import Footer from './Footer.jsx' + +it('is stylish', () => { + mount(