diff --git a/circle.yml b/circle.yml index 64d06b24..d42e9080 100644 --- a/circle.yml +++ b/circle.yml @@ -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 + no-workspace: true + record: false + store_artifacts: true + command: npm run check:links + - cypress/run: name: Example A11y requires: @@ -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: @@ -261,7 +288,7 @@ workflows: store_artifacts: true - cypress/run: - name: Test + name: Component Tests executor: cypress/base-12 parallelism: 4 requires: @@ -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" @@ -294,7 +322,8 @@ workflows: executor: cypress/base-12 requires: - Install - - Test + - Component Tests + - Lint Markdown links - Example A11y - Example Babel - Example Component Folder diff --git a/examples/using-babel-typescript/.npmrc b/examples/using-babel-typescript/.npmrc new file mode 100644 index 00000000..43c97e71 --- /dev/null +++ b/examples/using-babel-typescript/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/examples/using-babel-typescript/README.md b/examples/using-babel-typescript/README.md new file mode 100644 index 00000000..7ca76123 --- /dev/null +++ b/examples/using-babel-typescript/README.md @@ -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
{n}
+} +``` + +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() + }) +}) +``` diff --git a/examples/using-babel-typescript/babel.config.js b/examples/using-babel-typescript/babel.config.js new file mode 100644 index 00000000..91ccb7fc --- /dev/null +++ b/examples/using-babel-typescript/babel.config.js @@ -0,0 +1,27 @@ +module.exports = { + 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, + }, + ], + ], + }, + }, +} diff --git a/examples/using-babel-typescript/cypress.json b/examples/using-babel-typescript/cypress.json new file mode 100644 index 00000000..6d313c40 --- /dev/null +++ b/examples/using-babel-typescript/cypress.json @@ -0,0 +1,8 @@ +{ + "fixturesFolder": false, + "testFiles": "**/*spec.tsx", + "viewportWidth": 500, + "viewportHeight": 500, + "experimentalComponentTesting": true, + "componentFolder": "src" +} diff --git a/examples/using-babel-typescript/cypress/integration/spec.js b/examples/using-babel-typescript/cypress/integration/spec.js new file mode 100644 index 00000000..d815fd54 --- /dev/null +++ b/examples/using-babel-typescript/cypress/integration/spec.js @@ -0,0 +1,6 @@ +/// +describe('integration spec', () => { + it('works', () => { + expect(1).to.equal(1) + }) +}) diff --git a/examples/using-babel-typescript/cypress/plugins/index.js b/examples/using-babel-typescript/cypress/plugins/index.js new file mode 100644 index 00000000..070a1f84 --- /dev/null +++ b/examples/using-babel-typescript/cypress/plugins/index.js @@ -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 +} diff --git a/examples/using-babel-typescript/cypress/support/index.js b/examples/using-babel-typescript/cypress/support/index.js new file mode 100644 index 00000000..5d9ef5d1 --- /dev/null +++ b/examples/using-babel-typescript/cypress/support/index.js @@ -0,0 +1 @@ +require('cypress-react-unit-test/dist/hooks') diff --git a/examples/using-babel-typescript/images/dynamic.gif b/examples/using-babel-typescript/images/dynamic.gif new file mode 100644 index 00000000..d37a0fce Binary files /dev/null and b/examples/using-babel-typescript/images/dynamic.gif differ diff --git a/examples/using-babel-typescript/package.json b/examples/using-babel-typescript/package.json new file mode 100644 index 00000000..3604977d --- /dev/null +++ b/examples/using-babel-typescript/package.json @@ -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:../.." + } +} diff --git a/examples/using-babel-typescript/src/Component.spec.tsx b/examples/using-babel-typescript/src/Component.spec.tsx new file mode 100644 index 00000000..e10b830e --- /dev/null +++ b/examples/using-babel-typescript/src/Component.spec.tsx @@ -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() + + cy.get('.random').contains('777') + }) +}) diff --git a/examples/using-babel-typescript/src/Component.tsx b/examples/using-babel-typescript/src/Component.tsx new file mode 100644 index 00000000..11c88b5d --- /dev/null +++ b/examples/using-babel-typescript/src/Component.tsx @@ -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
{n}
+} + +export default Component diff --git a/examples/using-babel-typescript/src/calc.ts b/examples/using-babel-typescript/src/calc.ts new file mode 100644 index 00000000..fe2bb220 --- /dev/null +++ b/examples/using-babel-typescript/src/calc.ts @@ -0,0 +1 @@ +export const getRandomNumber = () => Math.round(Math.random() * 1000) diff --git a/examples/using-babel-typescript/tsconfig.json b/examples/using-babel-typescript/tsconfig.json new file mode 100644 index 00000000..d512fb8b --- /dev/null +++ b/examples/using-babel-typescript/tsconfig.json @@ -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*"] +} diff --git a/lib/index.ts b/lib/index.ts index 7fc96709..5d0e3ff3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -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`, @@ -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) @@ -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, + ) } } @@ -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() { + let value: T | undefined | null = null let error: Error | null = null const resolvers: any[] = [] @@ -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()) @@ -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) { @@ -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( diff --git a/plugins/babelrc/file-preprocessor.js b/plugins/babelrc/file-preprocessor.js index f3688593..97e1ee3e 100644 --- a/plugins/babelrc/file-preprocessor.js +++ b/plugins/babelrc/file-preprocessor.js @@ -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 = { + ...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)) { @@ -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)