Skip to content
This repository was archived by the owner on Mar 5, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 34 additions & 5 deletions circle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,27 @@ workflows:
- run:
name: Stop exclusive tests 1️⃣
command: npm run stop-only
- run:
name: Check markdown links ⚓️
command: npm run check:links
- run:
name: Build folder 🏗
command: npm run build

- cypress/run:
name: Lint Markdown links
filters:
branches:
only: main
executor: cypress/base-12
requires:
- Install
# notice a trick to avoid re-installing dependencies
# in this job - a do-nothing "install-command" parameter
install-command: echo 'Nothing to install in this job'
# we are not going to use results from this job anywhere else
Comment on lines +37 to +40
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What the motivation for using cypress/run instead of normal docker container with node?

Copy link
Contributor

Choose a reason for hiding this comment

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

caching, all OSS dependencies are present, restores the CircleCI workspace correctly. If you want to recreate this, see the expanded config at circle, it is giant https://app.circleci.com/pipelines/github/bahmutov/cypress-react-unit-test/635/config

this config is 330 lines, expanded 1000

Copy link
Contributor

Choose a reason for hiding this comment

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

Normally the above command would have "parallel" flag, in that case it skips the install. Since we are just reusing workspace from the previous install job, I have to hack skipping the install. I should probably just add a flag to our orb install: false to do this better

no-workspace: true
record: false
store_artifacts: true
command: npm run check:links

- cypress/run:
name: Example A11y
requires:
Expand All @@ -55,6 +69,19 @@ workflows:
command: npm test
store_artifacts: true

- cypress/run:
name: Example Babel + Typescript
requires:
- Install
executor: cypress/base-12
# each example installs "cypress-react-unit-test" as a local dependency (symlink)
install-command: npm install
verify-command: echo 'Already verified'
no-workspace: true
working_directory: examples/using-babel-typesceript
command: npm test
store_artifacts: true

- cypress/run:
name: Example React Scripts
requires:
Expand Down Expand Up @@ -261,7 +288,7 @@ workflows:
store_artifacts: true

- cypress/run:
name: Test
name: Component Tests
executor: cypress/base-12
parallelism: 4
requires:
Expand All @@ -275,6 +302,7 @@ workflows:
store_artifacts: true
# following examples from
# https://circleci.com/docs/2.0/parallelism-faster-jobs/
# TODO probably only run component tests and move integration sanity checks into own job
command: |
TESTFILES=$(circleci tests glob "cypress/{component,integration}/**/*spec.{js,jsx,ts,tsx}" | circleci tests split --total=4)
echo "Test files for this machine are $TESTFILES"
Expand All @@ -294,7 +322,8 @@ workflows:
executor: cypress/base-12
requires:
- Install
- Test
- Component Tests
- Lint Markdown links
- Example A11y
- Example Babel
- Example Component Folder
Expand Down
1 change: 1 addition & 0 deletions examples/using-babel-typescript/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
74 changes: 74 additions & 0 deletions examples/using-babel-typescript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# example: using-babel

> Component testing for typescript projects using Babel config with `@babel/preset-typescript`

![Example component test](images/dynamic.gif)

## Usage

1. Make sure the root project has been built .

```bash
# in the root of the project
npm install
npm run build
```

2. Run `npm install` in this folder to symlink the `cypress-react-unit-test` dependency.

```bash
# in this folder
npm install
```

3. Start Cypress

```bash
npm run cy:open
# or just run headless tests
npm test
```

## Specs

See spec files [src/\*.spec.js](src). The specs are bundled using [babel.config.js](babel.config.js) settings via [cypress/plugins/index.js](cypress/plugins/index.js) file that includes file preprocessor.

```js
// let's bundle spec files and the components they include using
// the same bundling settings as the project by loading .babelrc
const preprocessor = require('cypress-react-unit-test/plugins/babelrc')
module.exports = (on, config) => {
preprocessor(on, config)
// IMPORTANT to return the config object
// with the any changed environment variables
return config
}
```

## Mocking

During test runs, there is a Babel plugin that transforms ES6 imports into plain objects that can be stubbed using [cy.stub](https://on.cypress.io/stub). In essence

```ts
// component imports named ES6 import from "calc.js
import { getRandomNumber } from './calc'
const Component = () => {
// then calls it
const n = getRandomNumber()
return <div className="random">{n}</div>
}
```

The test can mock that import before mounting the component

```js
import Component from './Component'
import * as calc from './calc'
describe('Component', () => {
it('mocks call from the component', () => {
cy.stub(calc, 'getRandomNumber')
.returns(777)
mount(<Component />)
})
})
```
27 changes: 27 additions & 0 deletions examples/using-babel-typescript/babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Verified that it works also with babel.config.js. Should we rename this because 'cypress-react-unit-test/plugins/babelrc' looks confusing.

Copy link
Contributor

Choose a reason for hiding this comment

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

sure, rename it

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I've been not clear. I mean the name of plugin itself. It will require breaking change, right? Or just make a new alias name?

Copy link
Contributor

Choose a reason for hiding this comment

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

right - ok, in that case we probably better with an alias, like react-scripts is an alias to car.

presets: [
'@babel/preset-env',
'@babel/preset-typescript',
'@babel/preset-react',
],
plugins: ['@babel/plugin-proposal-class-properties'],
env: {
// place plugins for Cypress tests into "test" environment
// so that production bundle is not instrumented
test: {
plugins: [
// during Cypress tests we want to instrument source code
// to get code coverage from tests
'babel-plugin-istanbul',
// we also want to export ES6 modules as objects
// to allow mocking named imports
[
'@babel/plugin-transform-modules-commonjs',
{
loose: true,
},
],
],
},
},
}
8 changes: 8 additions & 0 deletions examples/using-babel-typescript/cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"fixturesFolder": false,
"testFiles": "**/*spec.tsx",
"viewportWidth": 500,
"viewportHeight": 500,
"experimentalComponentTesting": true,
"componentFolder": "src"
}
6 changes: 6 additions & 0 deletions examples/using-babel-typescript/cypress/integration/spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// <reference types="cypress" />
describe('integration spec', () => {
it('works', () => {
expect(1).to.equal(1)
})
})
10 changes: 10 additions & 0 deletions examples/using-babel-typescript/cypress/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// let's bundle spec files and the components they include using
// the same bundling settings as the project by loading .babelrc
// https://github.com/bahmutov/cypress-react-unit-test#install
const preprocessor = require('cypress-react-unit-test/plugins/babelrc')
module.exports = (on, config) => {
preprocessor(on, config)
// IMPORTANT to return the config object
// with the any changed environment variables
return config
}
1 change: 1 addition & 0 deletions examples/using-babel-typescript/cypress/support/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
require('cypress-react-unit-test/dist/hooks')
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions examples/using-babel-typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "example-using-babel",
"description": "Component testing for projects using Babel config",
"private": true,
"scripts": {
"test": "node ../../scripts/cypress-expect run --passing 17",
"cy:open": "../../node_modules/.bin/cypress open"
},
"devDependencies": {
"cypress-react-unit-test": "file:../.."
}
}
17 changes: 17 additions & 0 deletions examples/using-babel-typescript/src/Component.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from 'react'
import { Component } from './Component'
import * as calc from './calc'
import { mount } from 'cypress-react-unit-test'

// import the component and the file it imports
// stub the method on the imported "calc" and
// confirm the component renders the mock value
describe('Component', () => {
it('mocks call from the component', () => {
cy.stub(calc, 'getRandomNumber').returns(777)

mount(<Component />)

cy.get('.random').contains('777')
})
})
14 changes: 14 additions & 0 deletions examples/using-babel-typescript/src/Component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as React from 'react'
import { getRandomNumber } from './calc'

/**
* Example React component that imports `getRandomNumber`
* function from another file and uses it to show a random
* number in the UI.
*/
export const Component = () => {
const n = getRandomNumber()
return <div className="random">{n}</div>
}

export default Component
1 change: 1 addition & 0 deletions examples/using-babel-typescript/src/calc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const getRandomNumber = () => Math.round(Math.random() * 1000)
15 changes: 15 additions & 0 deletions examples/using-babel-typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"jsx": "react",
"baseUrl": ".",
"noEmit": true,
"target": "esnext",
"types": ["cypress"],
// ⚠️ required only for demo purposes, remove from code
"paths": {
"cypress-react-unit-test": ["../../lib/index.ts"]
}
},
"exclude": ["node_modules"],
"include": ["./src/**.ts*"]
}
28 changes: 17 additions & 11 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import { injectStylesBeforeElement } from './utils'

const rootId = 'cypress-root'

// @ts-ignore
const isComponentSpec = () => Cypress.spec.specType === 'component'

function checkMountModeEnabled() {
// @ts-ignore
if (!isComponentSpec()) {
throw new Error(
`In order to use mount or unmount functions please place the spec in component folder`,
Expand Down Expand Up @@ -68,7 +66,7 @@ export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => {
})
.then(injectStyles(options))
.then(() => {
const document = cy.state('document')
const document = cy.state('document') as Document
const reactDomToUse = options.ReactDom || ReactDOM

const el = document.getElementById(rootId)
Expand Down Expand Up @@ -113,7 +111,10 @@ export const mount = (jsx: React.ReactElement, options: MountOptions = {}) => {
logInstance.set('consoleProps', () => logConsoleProps)

if (el.children.length) {
logInstance.set('$el', el.children.item(0))
logInstance.set(
'$el',
(el.children.item(0) as unknown) as JQuery<HTMLElement>,
)
}
}

Expand Down Expand Up @@ -168,8 +169,8 @@ export const unmount = () => {

// mounting hooks inside a test component mostly copied from
// https://github.com/testing-library/react-hooks-testing-library/blob/master/src/pure.js
function resultContainer() {
let value: any = null
function resultContainer<T>() {
let value: T | undefined | null = null
let error: Error | null = null
const resolvers: any[] = []

Expand All @@ -185,7 +186,7 @@ function resultContainer() {
},
}

const updateResult = (val: any, err: Error | null = null) => {
const updateResult = (val: T | undefined, err: Error | null = null) => {
value = val
error = err
resolvers.splice(0, resolvers.length).forEach(resolve => resolve())
Expand All @@ -196,13 +197,18 @@ function resultContainer() {
addResolver: (resolver: any) => {
resolvers.push(resolver)
},
setValue: (val: any) => updateResult(val),
setValue: (val: T) => updateResult(val),
setError: (err: Error) => updateResult(undefined, err),
}
}

// @ts-ignore
function TestHook({ callback, onError, children }) {
type TestHookProps = {
callback: () => void
onError: (e: Error) => void
children: (...args: any[]) => any
}

function TestHook({ callback, onError, children }: TestHookProps) {
try {
children(callback())
} catch (err) {
Expand All @@ -225,7 +231,7 @@ function TestHook({ callback, onError, children }) {
*
* @see https://github.com/bahmutov/cypress-react-unit-test#advanced-examples
*/
export const mountHook = (hookFn: Function) => {
export const mountHook = (hookFn: (...args: any[]) => any) => {
const { result, setValue, setError } = resultContainer()

return mount(
Expand Down
11 changes: 10 additions & 1 deletion plugins/babelrc/file-preprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ const debug = require('debug')('cypress-react-unit-test')
const webpackPreprocessor = require('@cypress/webpack-preprocessor')
const { addImageRedirect } = require('../utils/add-image-redirect')

const wpPreprocessorOptions = {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Another question – do we need to keep using defaultOptions? Or we can just make our own

Copy link
Contributor

Choose a reason for hiding this comment

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

I would not change unless necessary

...webpackPreprocessor.defaultOptions,
}

wpPreprocessorOptions.webpackOptions.resolve = {
extensions: ['.js', '.ts', '.jsx', '.tsx', '.json'],
}

wpPreprocessorOptions.webpackOptions.module.rules[0].test = /\.(jsx|tsx|js|ts)?$/

// note: modifies the input object
function enableBabelrc(webpackOptions) {
if (!Array.isArray(webpackOptions.module.rules)) {
Expand Down Expand Up @@ -66,7 +76,6 @@ module.exports = config => {
config && config.env && config.env.coverage === false
debug('coverage is disabled? %o', { coverageIsDisabled })

const wpPreprocessorOptions = webpackPreprocessor.defaultOptions
enableBabelrc(wpPreprocessorOptions.webpackOptions)
debug('webpack options %o', wpPreprocessorOptions.webpackOptions)

Expand Down