Skip to content

Commit

Permalink
feat: React 18 support (#22876)
Browse files Browse the repository at this point in the history
* update cli exports

* add additional react adapters

* update system test infra to better cover react versions

* use idiomatic cy.mount and cy.unmount

* add additional test projects

* update tests

* add new modules

* remove dead code, organize code more

* add react 16 project

* update webpack to resolve react correctly

* add test for react 16

* update snaps

* add react adapters

* ignore cli/react files

* use official rollup plugin to bundle npm/react

* update yarn lock for webpack dev server tests

* update vite dev server projects

* update config

* remove console.log

* update tsconfig

* fix tests

* fix another test

* update snaps

* update snaps

* fix build

* remove react{16,17}, update tests

* update build

* add missing export

* update test

* fixing tests

* fixing tests

* update snaps

* update snaps again

* build artifacts on circle

* dont try to update rollup plugin

* update circle

* update

* add missing build step

* update deps, remove old code

* revert circle changes

* do not hoist deps from react18

* remove deps
  • Loading branch information
lmiller1990 authored Jul 22, 2022
1 parent 60fd568 commit f0d3a48
Show file tree
Hide file tree
Showing 73 changed files with 7,933 additions and 554 deletions.
2 changes: 1 addition & 1 deletion cli/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ build
# the sync-exported-npm-with-cli.js script
vue
vue2
react
react*
mount-utils
8 changes: 7 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@
"mount-utils",
"vue",
"react",
"vue2"
"vue2",
"react18"
],
"bin": {
"cypress": "bin/cypress"
Expand Down Expand Up @@ -141,6 +142,11 @@
"require": "./react/dist/cypress-react.cjs.js",
"types": "./react/dist/index.d.ts"
},
"./react18": {
"import": "./react18/dist/cypress-react.esm-bundler.js",
"require": "./react18/dist/cypress-react.cjs.js",
"types": "./react18/dist/index.d.ts"
},
"./mount-utils": {
"require": "./mount-utils/dist/index.js",
"types": "./mount-utils/dist/index.d.ts"
Expand Down
1 change: 1 addition & 0 deletions cli/scripts/post-build.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ shell.set('-e') // any error is fatal
const npmModulesToCopy = [
'mount-utils',
'react',
'react18',
'vue',
'vue2',
]
Expand Down
52 changes: 24 additions & 28 deletions npm/react/rollup.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import ts from 'rollup-plugin-typescript2'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
// CommonJS to easily share across packages
const ts = require('rollup-plugin-typescript2')
const { default: resolve } = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')

import pkg from './package.json'
const pkg = require('./package.json')

const banner = `
/**
Expand All @@ -16,17 +17,29 @@ function createEntry (options) {
const {
format,
input,
isBrowser,
} = options

const config = {
input,
external: [
'react',
'react-dom',
'react-dom/client',
],
plugins: [
resolve(), commonjs(),
resolve(),
commonjs(),
ts({
check: format === 'es',
tsconfigOverride: {
compilerOptions: {
declaration: format === 'es',
target: 'es5',
module: format === 'cjs' ? 'es2015' : 'esnext',
},
exclude: ['tests'],
},
}),
],
output: {
banner,
Expand All @@ -36,43 +49,26 @@ function createEntry (options) {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'react-dom/client': 'ReactDOM/client',
},
},
}

if (format === 'es') {
config.output.file = pkg.module
if (isBrowser) {
config.output.file = pkg.unpkg
}
}

if (format === 'cjs') {
config.output.file = pkg.main
}

// eslint-disable-next-line no-console
console.log(`Building ${format}: ${config.output.file}`)

config.plugins.push(
ts({
check: format === 'es' && isBrowser,
tsconfigOverride: {
compilerOptions: {
declaration: format === 'es',
target: 'es5', // not sure what this should be?
module: format === 'cjs' ? 'es2015' : 'esnext',
},
exclude: ['tests'],
},
}),
)

return config
}

export default [
createEntry({ format: 'es', input: 'src/index.ts', isBrowser: false }),
createEntry({ format: 'es', input: 'src/index.ts', isBrowser: true }),
createEntry({ format: 'iife', input: 'src/index.ts', isBrowser: true }),
createEntry({ format: 'cjs', input: 'src/index.ts', isBrowser: false }),
module.exports = [
createEntry({ format: 'es', input: 'src/index.ts' }),
createEntry({ format: 'cjs', input: 'src/index.ts' }),
]
207 changes: 207 additions & 0 deletions npm/react/src/createMount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import * as React from 'react'
import ReactDOM from 'react-dom'
import getDisplayName from './getDisplayName'
import {
injectStylesBeforeElement,
getContainerEl,
ROOT_SELECTOR,
setupHooks,
} from '@cypress/mount-utils'
import type { InternalMountOptions, InternalUnmountOptions, MountOptions, MountReturn, UnmountArgs } from './types'

/**
* Inject custom style text or CSS file or 3rd party style resources
*/
const injectStyles = (options: MountOptions) => {
return (): HTMLElement => {
const el = getContainerEl()

return injectStylesBeforeElement(options, document, el)
}
}

export let lastMountedReactDom: (typeof ReactDOM) | undefined

/**
* Create an `mount` function. Performs all the non-React-version specific
* behavior related to mounting. The React-version-specific code
* is injected. This helps us to maintain a consistent public API
* and handle breaking changes in React's rendering API.
*
* This is designed to be consumed by `npm/react{16,17,18}`, and other React adapters,
* or people writing adapters for third-party, custom adapters.
*/
export const makeMountFn = (
type: 'mount' | 'rerender',
jsx: React.ReactNode,
options: MountOptions = {},
rerenderKey?: string,
internalMountOptions?: InternalMountOptions,
): globalThis.Cypress.Chainable<MountReturn> => {
if (!internalMountOptions) {
throw Error('internalMountOptions must be provided with `render` and `reactDom` parameters')
}

// Get the display name property via the component constructor
// @ts-ignore FIXME
const componentName = getDisplayName(jsx.type, options.alias)
const displayName = options.alias || componentName

const jsxComponentName = `<${componentName} ... />`

const message = options.alias
? `${jsxComponentName} as "${options.alias}"`
: jsxComponentName

return cy
.then(injectStyles(options))
.then(() => {
const reactDomToUse = internalMountOptions.reactDom

lastMountedReactDom = reactDomToUse

const el = getContainerEl()

if (!el) {
throw new Error(
[
`[@cypress/react] 🔥 Hmm, cannot find root element to mount the component. Searched for ${ROOT_SELECTOR}`,
].join(' '),
)
}

const key = rerenderKey ??
// @ts-ignore provide unique key to the the wrapped component to make sure we are rerendering between tests
(Cypress?.mocha?.getRunner()?.test?.title as string || '') + Math.random()
const props = {
key,
}

const reactComponent = React.createElement(
options.strict ? React.StrictMode : React.Fragment,
props,
jsx,
)
// since we always surround the component with a fragment
// let's get back the original component
const userComponent = (reactComponent.props as {
key: string
children: React.ReactNode
}).children

internalMountOptions.render(reactComponent, el, reactDomToUse)

if (options.log !== false) {
Cypress.log({
name: type,
type: 'parent',
message: [message],
// @ts-ignore
$el: (el.children.item(0) as unknown) as JQuery<HTMLElement>,
consoleProps: () => {
return {
// @ts-ignore protect the use of jsx functional components use ReactNode
props: jsx.props,
description: type === 'mount' ? 'Mounts React component' : 'Rerenders mounted React component',
home: 'https://github.com/cypress-io/cypress',
}
},
}).snapshot('mounted').end()
}

return (
// Separate alias and returned value. Alias returns the component only, and the thenable returns the additional functions
cy.wrap<React.ReactNode>(userComponent, { log: false })
.as(displayName)
.then(() => {
return cy.wrap<MountReturn>({
component: userComponent,
rerender: (newComponent) => makeMountFn('rerender', newComponent, options, key, internalMountOptions),
unmount: internalMountOptions.unmount,
}, { log: false })
})
// by waiting, we delaying test execution for the next tick of event loop
// and letting hooks and component lifecycle methods to execute mount
// https://github.com/bahmutov/cypress-react-unit-test/issues/200
.wait(0, { log: false })
)
// Bluebird types are terrible. I don't think the return type can be carried without this cast
}) as unknown as globalThis.Cypress.Chainable<MountReturn>
}

/**
* Create an `unmount` function. Performs all the non-React-version specific
* behavior related to unmounting.
*
* This is designed to be consumed by `npm/react{16,17,18}`, and other React adapters,
* or people writing adapters for third-party, custom adapters.
*/
export const makeUnmountFn = (options: UnmountArgs, internalUnmountOptions: InternalUnmountOptions) => {
return cy.then(() => {
return cy.get(ROOT_SELECTOR, { log: false }).then(($el) => {
if (lastMountedReactDom) {
internalUnmountOptions.unmount($el[0])
const wasUnmounted = internalUnmountOptions.unmount($el[0])

if (wasUnmounted && options.log) {
Cypress.log({
name: 'unmount',
type: 'parent',
message: [options.boundComponentMessage ?? 'Unmounted component'],
consoleProps: () => {
return {
description: 'Unmounts React component',
parent: $el[0],
home: 'https://github.com/cypress-io/cypress',
}
},
})
}
}
})
})
}

// Cleanup before each run
// NOTE: we cannot use unmount here because
// we are not in the context of a test
const preMountCleanup = () => {
const el = getContainerEl()

if (el && lastMountedReactDom) {
lastMountedReactDom.unmountComponentAtNode(el)
}
}

const _mount = (jsx: React.ReactNode, options: MountOptions = {}) => makeMountFn('mount', jsx, options)

export const createMount = (defaultOptions: MountOptions) => {
return (
element: React.ReactElement,
options?: MountOptions,
) => {
return _mount(element, { ...defaultOptions, ...options })
}
}

/** @deprecated Should be removed in the next major version */
// TODO: Remove
export default _mount

export interface JSX extends Function {
displayName: string
}

// Side effects from "import { mount } from '@cypress/<my-framework>'" are annoying, we should avoid doing this
// by creating an explicit function/import that the user can register in their 'component.js' support file,
// such as:
// import 'cypress/<my-framework>/support'
// or
// import { registerCT } from 'cypress/<my-framework>'
// registerCT()
// Note: This would be a breaking change

// it is required to unmount component in beforeEach hook in order to provide a clean state inside test
// because `mount` can be called after some preparation that can side effect unmount
// @see npm/react/cypress/component/advanced/set-timeout-example/loading-indicator-spec.js
setupHooks(preMountCleanup)
2 changes: 1 addition & 1 deletion npm/react/src/getDisplayName.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { JSX } from './mount'
import { JSX } from './createMount'

const cachedDisplayNames: WeakMap<JSX, string> = new WeakMap()

Expand Down
4 changes: 4 additions & 0 deletions npm/react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export * from './createMount'

export * from './mount'

export * from './mountHook'

export * from './types'
Loading

5 comments on commit f0d3a48

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f0d3a48 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.4.0/linux-x64/develop-f0d3a4867907bf6e60468510daa883ccc8dcfb63/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f0d3a48 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the linux arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.4.0/linux-arm64/develop-f0d3a4867907bf6e60468510daa883ccc8dcfb63/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f0d3a48 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin arm64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.4.0/darwin-arm64/develop-f0d3a4867907bf6e60468510daa883ccc8dcfb63/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f0d3a48 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the darwin x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.4.0/darwin-x64/develop-f0d3a4867907bf6e60468510daa883ccc8dcfb63/cypress.tgz

@cypress-bot
Copy link
Contributor

@cypress-bot cypress-bot bot commented on f0d3a48 Jul 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circle has built the win32 x64 version of the Test Runner.

Learn more about this pre-release platform-specific build at https://on.cypress.io/installing-cypress#Install-pre-release-version.

Run this command to install the pre-release locally:

npm install https://cdn.cypress.io/beta/npm/10.4.0/win32-x64/develop-f0d3a4867907bf6e60468510daa883ccc8dcfb63/cypress.tgz

Please sign in to comment.