chore: cutting over system-tests and Cypress to use the new CT Object API (#21079)

* removing vite-dev-server local dependency from react-vite-ts-configured system test

* moving some CRA examples over to use the object api for setup

* fixing issue where function API was broken by object API for cy config + devservers

* adding deeply nested react import to project-fixtures for cra

* finishes cutting over cypress/react for sys tests

* chore: adding circle for this feature branch

* chore: moving over many vue + vite system tests to use object API instead of function API (#21080)

* doing webpack-dev-server cutovers

* removing more webpack-dev-server refrences

* fixing snapshots

* bumping yarn.lock

* wip

* fix test

* fix assertion

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>

* feat: removing all references for "fresh" dev servers (webpack-dev-server-fresh and vite-dev-server-fresh) (#21094)

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
Co-authored-by: Zachary Williams <ZachJW34@gmail.com>

* chore: add dev-servers as deps to server to be included in the binary (#21091)

* fix bad merge

* fix next types and webpack-dev-server- resolve

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
Co-authored-by: Zachary Williams <ZachJW34@gmail.com>
This commit is contained in:
Jess
2022-04-20 01:57:19 -04:00
committed by GitHub
parent 10dfccc674
commit b326693879
848 changed files with 11671 additions and 110677 deletions
+2 -2
View File
@@ -80,8 +80,8 @@ system-tests/lib/fixtureDirs.ts
/npm/design-system/cypress/videos
/npm/design-system/.babel-cache
# from npm/webpack-dev-server-fresh
/npm/webpack-dev-server-fresh/cypress/videos
# from npm/webpack-dev-server
/npm/webpack-dev-server/cypress/videos
# from runner-ct
/packages/runner-ct/cypress/screenshots
+21 -29
View File
@@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters
only:
- develop
- 10.0-release
- zachw/add-dev-server-deps
- chore/cutover-to-bundled-react-mount
# 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
@@ -47,7 +47,7 @@ macWorkflowFilters: &mac-workflow-filters
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ '10.0-release', << pipeline.git.branch >> ]
- equal: [ zachw/add-dev-server-deps, << pipeline.git.branch >> ]
- equal: [ chore/cutover-to-bundled-react-mount, << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -57,7 +57,8 @@ windowsWorkflowFilters: &windows-workflow-filters
or:
- equal: [ develop, << pipeline.git.branch >> ]
- equal: [ '10.0-release', << pipeline.git.branch >> ]
- equal: [ zachw/add-dev-server-deps, << pipeline.git.branch >> ]
- equal: [ chore/cutover-to-bundled-react-mount, << pipeline.git.branch >> ]
- equal: [ 'unify-1036-windows-test-projects', << pipeline.git.branch >> ]
- matches:
pattern: "-release$"
value: << pipeline.git.branch >>
@@ -1101,8 +1102,8 @@ jobs:
run-frontend-shared-component-tests-chrome,
run-launchpad-component-tests-chrome,
run-launchpad-integration-tests-chrome,
run-webpack-dev-server-fresh-integration-tests,
run-vite-dev-server-fresh-integration-tests
run-webpack-dev-server-integration-tests,
run-vite-dev-server-integration-tests
- run:
command: |
PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \
@@ -1161,8 +1162,10 @@ jobs:
- run: yarn test-scripts
# make sure our snapshots are compared correctly
- run: yarn test-mocha-snapshot
# run @cypress/design-system before other packages are built
- run: yarn lerna run build-prod --scope \"@cypress/design-system\"
# make sure packages with TypeScript can be transpiled to JS
- run: yarn lerna run build-prod --stream
- run: yarn lerna run build-prod --stream --ignore \"@cypress/design-system\"
# run unit tests from each individual package
- run: yarn test
# run type checking for each individual package
@@ -1437,7 +1440,7 @@ jobs:
path: /tmp/artifacts
- store-npm-logs
run-webpack-dev-server-fresh-integration-tests:
run-webpack-dev-server-integration-tests:
<<: *defaults
# parallelism: 3 TODO: Add parallelism once we have more specs
steps:
@@ -1451,15 +1454,15 @@ jobs:
PERCY_ENABLE=${PERCY_TOKEN:-0} \
PERCY_PARALLEL_TOTAL=-1 \
yarn percy exec --parallel -- -- \
yarn cypress:run --record --parallel --group webpack-dev-server-fresh
working_directory: npm/webpack-dev-server-fresh
yarn cypress:run --record --parallel --group webpack-dev-server
working_directory: npm/webpack-dev-server
- store_test_results:
path: /tmp/cypress
- store_artifacts:
path: /tmp/artifacts
- store-npm-logs
run-vite-dev-server-fresh-integration-tests:
run-vite-dev-server-integration-tests:
<<: *defaults
# parallelism: 3 TODO: Add parallelism once we have more specs
steps:
@@ -1473,8 +1476,8 @@ jobs:
PERCY_ENABLE=${PERCY_TOKEN:-0} \
PERCY_PARALLEL_TOTAL=-1 \
yarn percy exec --parallel -- -- \
yarn cypress:run --record --parallel --group vite-dev-server-fresh
working_directory: npm/vite-dev-server-fresh
yarn cypress:run --record --parallel --group vite-dev-server
working_directory: npm/vite-dev-server
- store_test_results:
path: /tmp/cypress
- store_artifacts:
@@ -1548,7 +1551,7 @@ jobs:
command: yarn workspace @cypress/webpack-dev-server test
- run:
name: Run tests
command: yarn workspace @cypress/webpack-dev-server-fresh test
command: yarn workspace @cypress/webpack-dev-server test
npm-vite-dev-server:
<<: *defaults
@@ -1556,7 +1559,7 @@ jobs:
- restore_cached_workspace
- run:
name: Run tests
command: yarn test --reporter mocha-multi-reporters --reporter-options configFile=../../mocha-reporter-config.json
command: yarn test
working_directory: npm/vite-dev-server
- store_test_results:
path: npm/vite-dev-server/test_results
@@ -1582,14 +1585,6 @@ jobs:
name: Type Check
command: yarn typecheck
working_directory: npm/vue
- run:
name: Run component tests
command: yarn test:ci:ct
working_directory: npm/vue
- run:
name: Run e2e tests
command: yarn test:ci:e2e
working_directory: npm/vue
- store_test_results:
path: npm/vue/test_results
- store_artifacts:
@@ -1645,7 +1640,7 @@ jobs:
command: yarn workspace @cypress/react build
- run:
name: Run tests
command: yarn test-ci
command: yarn test
working_directory: npm/react
- store_test_results:
path: npm/react/test_results
@@ -1668,9 +1663,6 @@ jobs:
steps:
- restore_cached_workspace
- run: yarn workspace create-cypress-tests build
- run:
name: Run unit test
command: yarn workspace create-cypress-tests test
npm-eslint-plugin-dev:
<<: *defaults
@@ -1727,7 +1719,7 @@ jobs:
- run:
name: Check current branch to persist artifacts
command: |
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "zachw/add-dev-server-deps" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then
if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "chore/cutover-to-bundled-react-mount" && "$CIRCLE_BRANCH" != "unify-1036-windows-test-projects" && "$CIRCLE_BRANCH" != "10.0-release" ]]; then
echo "Not uploading artifacts or posting install comment for this branch."
circleci-agent step halt
fi
@@ -2270,11 +2262,11 @@ linux-workflow: &linux-workflow
percy: true
requires:
- build
- run-webpack-dev-server-fresh-integration-tests:
- run-webpack-dev-server-integration-tests:
context: [test-runner:cypress-record-key, test-runner:percy]
requires:
- system-tests-node-modules-install
- run-vite-dev-server-fresh-integration-tests:
- run-vite-dev-server-integration-tests:
context: [test-runner:cypress-record-key, test-runner:percy]
requires:
- system-tests-node-modules-install
+12 -12
View File
@@ -6,7 +6,7 @@ NOTE: this is not published on npm yet. It's a work in progress. Consider [Cypre
](https://github.com/jscutlery/test-utils/tree/main/packages/cypress-angular) by [JS Cutlery](https://github.com/jscutlery) for a version that's currently working and available on npm.
```shell
npm install -D cypress @cypress/angular @cypress/webpack-dev-server
npm install -D cypress @cypress/angular
```
Ensure you have a version of Cypress > 7.
@@ -32,21 +32,21 @@ module.exports = {
}
```
Configure `cypress/plugins/index.js` to transpile Angular code.
Configure `cypress.config.js` to transpile Angular code.
```javascript
import { defineConfig } from 'cypress'
import * as webpackConfig from './webpack.config';
const { startDevServer } = require('@cypress/webpack-dev-server');
module.exports = (on, config) => {
on('dev-server:start', (options) =>
startDevServer({
options,
webpackConfig,
}),
);
return config;
};
export default defineConfig({
component: {
devServer: {
bundler: 'webpack',
webpackConfig
}
}
})
```
The `webpack.config.ts` file is [here](cypress/plugins/webpack.config.ts).
+6 -3
View File
@@ -1,5 +1,4 @@
import { defineConfig } from 'cypress'
import { devServer } from '@cypress/webpack-dev-server'
import * as webpackConfig from './cypress/plugins/webpack.config'
export default defineConfig({
@@ -12,7 +11,11 @@ export default defineConfig({
setupNodeEvents (on, config) {
return require('./cypress/plugins')(on, config)
},
devServer,
devServerConfig: { webpackConfig },
// @ts-ignore TODO: need to add the ability to define framework not
// in list w/o types failing...
devServer: {
bundler: 'webpack',
webpackConfig,
},
},
})
@@ -97,8 +97,8 @@ module.exports = {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'test'),
}),
new webpack.ContextReplacementPlugin(
/\@angular(\\|\/)core(\\|\/)f?esm5/,
path.join(__dirname, './src'),
/angular(\\|\/)core(\\|\/)(@angular|fesm2015)/,
path.join(__dirname, '..', '..', 'src'),
),
],
performance: {
+1 -3
View File
@@ -13,7 +13,7 @@
"app-start": "ng serve",
"app-build": "ng build",
"test": "yarn cy:run",
"test-ci": "yarn cy:run",
"test-ci": "echo not testing in CI because process never exits",
"postinstall": "patch-package"
},
"dependencies": {
@@ -38,7 +38,6 @@
"@angular/platform-browser-dynamic": "11.2.13",
"@angular/router": "11.2.13",
"@cypress/code-coverage": "3.9.5",
"@cypress/webpack-dev-server": "0.0.0-development",
"@cypress/webpack-preprocessor": "5.7.0",
"@types/cypress-image-snapshot": "3.1.5",
"@types/node": "8.10.66",
@@ -49,7 +48,6 @@
"cypress": "0.0.0-development",
"cypress-image-snapshot": "4.0.1",
"html-loader": "2.1.2",
"html-webpack-plugin": "5.3.1",
"istanbul-instrumenter-loader": "3.0.1",
"ng-inline-svg": "12.1.0",
"ngx-build-plus": "11.0.0",
+7 -4
View File
@@ -1,6 +1,6 @@
const { devServer } = require('@cypress/vite-dev-server')
const { defineConfig } = require('cypress')
module.exports = {
module.exports = defineConfig({
viewportWidth: 1024,
viewportHeight: 800,
video: false,
@@ -14,6 +14,9 @@ module.exports = {
'**/__snapshots__/*',
'**/__image_snapshots__/*',
],
devServer,
devServer: {
framework: 'react',
bundler: 'vite',
},
},
}
})
-2
View File
@@ -40,8 +40,6 @@
"@babel/preset-env": "7.4.5",
"@babel/preset-react": "7.0.0",
"@babel/preset-typescript": "7.10.4",
"@cypress/react": "0.0.0-development",
"@cypress/vite-dev-server": "0.0.0-development",
"@packages/web-config": "0.0.0-development",
"@react-types/button": "^3.3.1",
"@react-types/shared": "^3.5.0",
@@ -1,5 +1,5 @@
import * as React from 'react'
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { fas } from '@fortawesome/free-solid-svg-icons'
@@ -1,6 +1,6 @@
import React from 'react'
import { CypressLogo } from './CypressLogo/CypressLogo'
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fab } from '@fortawesome/free-brands-svg-icons'
@@ -1,5 +1,5 @@
import * as React from 'react'
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import { composeStories } from '@storybook/testing-react'
import * as stories from './CollapsibleGroup.stories'
@@ -1,5 +1,5 @@
import React from 'react'
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import { FileTree } from './FileTree'
import { mountAndSnapshot } from 'util/testing'
@@ -1,5 +1,5 @@
import * as React from 'react'
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import { SearchInput } from './SearchInput'
import { mountAndSnapshot } from 'util/testing'
+1 -1
View File
@@ -1,4 +1,4 @@
import { mount } from '@cypress/react'
import { mount } from 'cypress/react'
import React from 'react'
export const mountAndSnapshot =
-1
View File
@@ -239,7 +239,6 @@ Repo | Description
[bahmutov/integration-tests](https://github.com/bahmutov/integration-tests) | Example based on blog post [React Integration Testing: Greater Coverage, Fewer Tests](https://css-tricks.com/react-integration-testing-greater-coverage-fewer-tests/)
[mobx-react-typescript-boilerplate](https://github.com/bahmutov/mobx-react-typescript-boilerplate) | Fork of the official Mobx example, shows clock control
[bahmutov/test-react-hook-form](https://github.com/bahmutov/test-react-hook-form) | Testing forms created using [react-hook-form](https://github.com/react-hook-form/react-hook-form)
[bahmutov/react-with-rollup](https://github.com/bahmutov/react-with-rollup) | Testing a React application bundled with Rollup by using [@bahmutov/cy-rollup](https://github.com/bahmutov/cy-rollup) preprocessor
[bahmutov/testing-react-example](https://github.com/bahmutov/testing-react-example) | Described in blog post [Test React Component with @cypress/react Example](https://dev.to/bahmutov/test-react-component-with-@cypress/react-example-4d99)
[ejected-react-scripts-example](https://github.com/bahmutov/ejected-react-scripts-example) | Using component testing after ejecting `react-scripts`
[tic-tac-toe](https://github.com/bahmutov/react-tic-tac-toe-example) | Component and unit tests for Tic-Tac-Toe, read [Tic-Tac-Toe Component Tests](https://glebbahmutov.com/blog/tic-tac-toe-component-tests/)
+3 -61
View File
@@ -1,6 +1,3 @@
// @ts-check
const { devServer } = require('@cypress/webpack-dev-server')
module.exports = {
'viewportWidth': 400,
'viewportHeight': 400,
@@ -16,64 +13,9 @@ module.exports = {
'**/__image_snapshots__/*',
'examples/**/*',
],
devServer (cypressDevServerConfig, devServerConfig) {
const path = require('path')
const babelConfig = require('./babel.config.js')
const webpackConfig = {
resolve: {
extensions: ['.js', '.ts', '.jsx', '.tsx'],
},
mode: 'development',
devtool: false,
output: {
publicPath: '/',
chunkFilename: '[name].bundle.js',
},
module: {
rules: [
{
test: /\.(js|jsx|mjs|ts|tsx)$/,
loader: 'babel-loader',
options: { ...babelConfig, cacheDirectory: path.resolve(__dirname, '..', '..', '.babel-cache') },
},
{
test: /\.modules\.css$/i,
exclude: [/node_modules/],
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true,
},
},
],
},
{
test: /\.css$/,
exclude: [/node_modules/, /\.modules\.css$/i],
use: ['style-loader', 'css-loader'],
},
{
// some of our examples import SVG
test: /\.svg$/,
loader: 'svg-url-loader',
},
{
// some of our examples import SVG
test: /\.svg$/,
loader: 'svg-url-loader',
},
{
test: /\.(png|jpg)$/,
use: ['file-loader'],
},
],
},
}
return devServer(cypressDevServerConfig, { webpackConfig })
devServer: {
framework: 'react',
bundler: 'vite',
},
},
}
@@ -1,7 +0,0 @@
# cy-api test example
Using component tests with [cy-api](https://github.com/bahmutov/cy-api) tests. Read [Black box API testing with server logs](https://glebbahmutov.com/blog/api-testing-with-sever-logs/)
![API tests](images/api-tests.png)
Note: currently the API and component tests do not clear the HTML created by each other
Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

@@ -1,53 +0,0 @@
/// <reference types="cypress" />
/// <reference types="../../lib" />
/// <reference types="@bahmutov/cy-api" />
import { Users } from './users.jsx'
import React from 'react'
import { mount } from '@cypress/react'
// adds cy.api command
import '@bahmutov/cy-api/support'
// mixes component and API tests
describe('Component and API tests', () => {
it('fetches and shows 3 users', () => {
// no mocking, just real request to the backend REST endpoint
mount(<Users />)
// fetching users can take a while
cy.get('li', { timeout: 20000 }).should('have.length', 3)
})
it('checks if API responds with a list of users', () => {
cy.api({
// same or similar URL to the one the component is using
url: 'https://jsonplaceholder.cypress.io/users?_limit=3',
})
.its('body')
.should('have.length', 3)
})
// another component test
it('shows stubbed users', () => {
cy.stub(window, 'fetch').resolves({
json: cy
.stub()
.resolves([{ id: 101, name: 'Test User' }])
.as('users'),
})
// no mocking, just real request to the backend REST endpoint
mount(<Users />)
cy.get('li').should('have.length', 1)
cy.get('@users').should('have.been.calledOnce')
})
it('fetches one user from API', () => {
cy.api({
url: 'https://jsonplaceholder.cypress.io/users/1',
})
.its('body')
.should('include', {
id: 1,
name: 'Leanne Graham',
})
})
})
@@ -1,34 +0,0 @@
import React from 'react'
export class Users extends React.Component {
constructor (props) {
super(props)
this.state = {
users: [],
}
}
componentDidMount () {
fetch('https://jsonplaceholder.cypress.io/users?_limit=3')
.then((response) => {
return response.json()
})
.then((list) => {
this.setState({
users: list,
})
})
}
render () {
return (
<div>
{this.state.users.map((user) => (
<li key={user.id}>
<strong>{user.id}</strong> - {user.name}
</li>
))}
</div>
)
}
}
@@ -1,11 +0,0 @@
body {
width: 100vw;
height: 100vh;
background: linear-gradient(180deg, #d309e1 0%, rgb(156, 26, 255) 100%);
overflow: hidden;
padding: 0;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
@@ -1,29 +0,0 @@
import * as React from 'react'
import { Motion } from './Motion'
import { mount } from '@cypress/react'
describe('framer-motion', () => {
it('Renders component and retries the animation', () => {
mount(<Motion />)
cy.get('[data-testid=\'motion\']').should('have.css', 'border-radius', '50%')
cy.get('[data-testid=\'motion\']').should('have.css', 'border-radius', '20%')
})
// NOTE: looks like cy.tick issue. Refer to the https://github.com/bahmutov/cypress-react-unit-test/issues/420
it.skip('Mocks setTimeout and requestAnimationFrame', () => {
cy.clock()
mount(<Motion />)
// CI is slow, so check only the approximate values
cy.tick(800)
cy.get('[data-testid=\'motion\']').within((element) => {
expect(parseInt(element.css('borderRadius'))).to.equal(43)
})
cy.tick(100)
cy.get('[data-testid=\'motion\']').within((element) => {
expect(parseInt(element.css('borderRadius'))).to.equal(48)
})
})
})
@@ -1,24 +0,0 @@
import './Motion.css'
import * as React from 'react'
import { motion } from 'framer-motion'
export const Motion = () => {
return (
<motion.div
data-testid="motion"
style={{ width: 100, height: 100, backgroundColor: 'white' }}
animate={{
scale: [1, 2, 2, 1, 1],
rotate: [0, 0, 270, 270, 0],
borderRadius: ['20%', '20%', '50%', '50%', '20%'],
}}
transition={{
duration: 2,
ease: 'easeInOut',
times: [0, 0.2, 0.5, 0.8, 1],
loop: false,
repeatDelay: 1,
}}
/>
)
}
@@ -1,12 +0,0 @@
import * as React from 'react'
import { I18nextProvider } from 'react-i18next'
import i18n from './i18n'
import { LocalizedComponent } from './LocalizedComponent'
export function App () {
return (
<I18nextProvider i18n={i18n}>
<LocalizedComponent count={15} name="SomeUserName" />
</I18nextProvider>
)
}
@@ -1,19 +0,0 @@
import * as React from 'react'
import { Trans } from 'react-i18next'
interface LocalizedComponentProps {
name: string
count: number
}
// See ./App.tsx for localization setup
export function LocalizedComponent ({ name, count }: LocalizedComponentProps) {
return (
<Trans
i18nKey={count === 1 ? 'userMessagesUnread' : 'userMessagesUnread_plural'}
count={count}
>
Hello <strong> {{ name }} </strong>, you have {{ count }} unread message{' '}
</Trans>
)
}
@@ -1,15 +0,0 @@
## Localization Example
This example uses [react-i18next](https://react.i18next.com/) for app localization. Make sure that in "real life" application locale related setup performs at the root of application ([App.tsx](./App.tsx)) and the components are using context for localization.
Thats why in tests we also need to wrap our component with the same provider as our application. Using function composition we can create our own `mount` function which wraps the component with all required providers:
```js
const localizedMount = (node, { locale }) => {
mount(
<I18nextProvider i18n={i18n.cloneInstance({ lng: locale })}>
{node}
</I18nextProvider>,
)
}
```
@@ -1,33 +0,0 @@
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
i18n
.use(initReactI18next) // passes i18n down to react-i18next
.init({
resources: {
en: {
translation: {
userMessagesUnread:
'Hello <1>{{name}}</1>, you have {{count}} unread message.',
userMessagesUnread_plural:
'Hello <1>{{name}}</1>, you have {{count}} unread messages.',
},
},
ru: {
translation: {
userMessagesUnread:
'Привет, <1>{{name}}</1>, y тебя {{count}} непрочитанное сообщение.',
userMessagesUnread_plural:
'Привет, <1>{{name}}</1>, y тебя {{count}} непрочитанных сообщений.',
},
},
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
})
export default i18n
@@ -1,48 +0,0 @@
/// <reference types="cypress" />
import * as React from 'react'
import i18n from './i18n'
import { LocalizedComponent } from './LocalizedComponent'
import { mount } from '@cypress/react'
import { I18nextProvider } from 'react-i18next'
describe('i18n', () => {
const localizedMount = (node, { locale }) => {
mount(
<I18nextProvider i18n={i18n.cloneInstance({ lng: locale })}>
{node}
</I18nextProvider>,
)
}
it('Plural in en', () => {
localizedMount(<LocalizedComponent count={15} name="Josh" />, {
locale: 'en',
})
cy.contains('Hello Josh, you have 15 unread messages.')
})
it('Single in en', () => {
localizedMount(<LocalizedComponent count={1} name="Josh" />, {
locale: 'en',
})
cy.contains('Hello Josh, you have 1 unread message.')
})
it('Plural in ru', () => {
localizedMount(<LocalizedComponent count={15} name="Костя" />, {
locale: 'ru',
})
cy.contains('Привет, Костя, y тебя 15 непрочитанных сообщений.')
})
it('Single in ru', () => {
localizedMount(<LocalizedComponent count={1} name="Костя" />, {
locale: 'ru',
})
cy.contains('Привет, Костя, y тебя 1 непрочитанное сообщение.')
})
})
@@ -1 +0,0 @@
Examples from https://material-ui.com/
@@ -1,58 +0,0 @@
/// <reference types="cypress" />
import { mount } from '@cypress/react'
// select example from
// https://material-ui.com/components/autocomplete/
/* eslint-disable no-use-before-define */
import React from 'react'
import TextField from '@material-ui/core/TextField'
import Autocomplete from '@material-ui/lab/Autocomplete'
import { top100Films } from './top-100-movies'
export default function ComboBox () {
return (
<Autocomplete
id="combo-box-demo"
options={top100Films}
getOptionLabel={(option) => option.title}
style={{ width: 300 }}
renderInput={(params) => {
return (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)
}}
/>
)
}
it('finds my favorite movie', () => {
cy.viewport(500, 700)
mount(
<Autocomplete
id="combo-box-demo"
options={top100Films}
getOptionLabel={(option) => option.title}
style={{ width: 300 }}
renderInput={(params) => {
return (
<TextField {...params} label="Combo box" variant="outlined" fullWidth />
)
}}
/>,
{
stylesheets: [
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap',
'https://fonts.googleapis.com/icon?family=Material+Icons',
],
},
)
cy.get('#combo-box-demo').click()
cy.focused().type('god')
cy.contains('The Godfather')
.should('be.visible')
.and('have.class', 'MuiAutocomplete-option')
.click()
cy.get('#combo-box-demo').should('have.value', 'The Godfather')
})
@@ -1,19 +0,0 @@
import React from 'react'
import { mount } from '@cypress/react'
import Button from '@material-ui/core/Button'
it('renders a button', () => {
mount(
<Button variant="contained" color="primary">
Hello World
</Button>,
)
})
it('renders a button with an icon', () => {
mount(
<Button variant="contained" color="primary" startIcon="⛹️">
Hello World
</Button>,
)
})
@@ -1,8 +0,0 @@
import React from 'react'
import { mount } from '@cypress/react'
import CheckboxLabels from './checkbox-labels'
it('renders checkboxes', () => {
cy.viewport(600, 600)
mount(<CheckboxLabels />)
})
@@ -1,116 +0,0 @@
import React from 'react'
import { withStyles } from '@material-ui/core/styles'
import { green } from '@material-ui/core/colors'
import FormGroup from '@material-ui/core/FormGroup'
import FormControlLabel from '@material-ui/core/FormControlLabel'
import Checkbox from '@material-ui/core/Checkbox'
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
import CheckBoxIcon from '@material-ui/icons/CheckBox'
import Favorite from '@material-ui/icons/Favorite'
import FavoriteBorder from '@material-ui/icons/FavoriteBorder'
const GreenCheckbox = withStyles({
root: {
color: green[400],
'&$checked': {
color: green[600],
},
},
checked: {},
})((props) => <Checkbox color="default" {...props} />)
export default function CheckboxLabels () {
const [state, setState] = React.useState({
checkedA: true,
checkedB: true,
checkedF: true,
checkedG: true,
})
const handleChange = (name) => {
return (event) => {
setState({ ...state, [name]: event.target.checked })
}
}
return (
<FormGroup row>
<FormControlLabel
control={
<Checkbox
checked={state.checkedA}
onChange={handleChange('checkedA')}
value="checkedA"
/>
}
label="Secondary"
/>
<FormControlLabel
control={
<Checkbox
checked={state.checkedB}
onChange={handleChange('checkedB')}
value="checkedB"
color="primary"
/>
}
label="Primary"
/>
<FormControlLabel
control={<Checkbox value="checkedC" />}
label="Uncontrolled"
/>
<FormControlLabel
disabled
control={<Checkbox value="checkedD" />}
label="Disabled"
/>
<FormControlLabel
disabled
control={<Checkbox checked value="checkedE" />}
label="Disabled"
/>
<FormControlLabel
control={
<Checkbox
checked={state.checkedF}
onChange={handleChange('checkedF')}
value="checkedF"
indeterminate
/>
}
label="Indeterminate"
/>
<FormControlLabel
control={
<GreenCheckbox
checked={state.checkedG}
onChange={handleChange('checkedG')}
value="checkedG"
/>
}
label="Custom color"
/>
<FormControlLabel
control={
<Checkbox
icon={<FavoriteBorder />}
checkedIcon={<Favorite />}
value="checkedH"
/>
}
label="Custom icon"
/>
<FormControlLabel
control={
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
value="checkedI"
/>
}
label="Custom size"
/>
</FormGroup>
)
}
@@ -1,55 +0,0 @@
import React from 'react'
import { makeStyles } from '@material-ui/core/styles'
import List from '@material-ui/core/List'
import ListItem from '@material-ui/core/ListItem'
import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import Divider from '@material-ui/core/Divider'
import InboxIcon from '@material-ui/icons/Inbox'
import DraftsIcon from '@material-ui/icons/Drafts'
const useStyles = makeStyles((theme) => {
return {
root: {
width: '100%',
maxWidth: 360,
backgroundColor: theme.palette.background.paper,
},
}
})
function ListItemLink (props) {
return <ListItem button component="a" {...props} />
}
export default function SimpleList () {
const classes = useStyles()
return (
<div className={classes.root}>
<List component="nav" aria-label="main mailbox folders">
<ListItem button>
<ListItemIcon>
<InboxIcon />
</ListItemIcon>
<ListItemText primary="Inbox" />
</ListItem>
<ListItem button>
<ListItemIcon>
<DraftsIcon />
</ListItemIcon>
<ListItemText primary="Drafts" />
</ListItem>
</List>
<Divider />
<List component="nav" aria-label="secondary mailbox folders">
<ListItem button>
<ListItemText primary="Trash" />
</ListItem>
<ListItemLink href="#simple-list">
<ListItemText primary="Spam" />
</ListItemLink>
</List>
</div>
)
}
@@ -1,28 +0,0 @@
/// <reference types="cypress" />
import React from 'react'
import { mount } from '@cypress/react'
import ListItem from '@material-ui/core/ListItem'
import { ListItemText } from '@material-ui/core'
import SimpleList from './list-demo'
it('renders a list item', () => {
mount(
<ListItem>
<ListItemText primary={'my example list item'} />
</ListItem>,
)
cy.contains('my example list item')
})
// demo from https://material-ui.com/components/lists/
it('renders full list', () => {
cy.viewport(500, 800)
mount(<SimpleList />)
cy.contains('Drafts')
.click()
.wait(1000)
.click()
.wait(1000)
.click()
})
@@ -1,269 +0,0 @@
import { mount } from '@cypress/react'
// select example from
// https://material-ui.com/components/selects/
import React from 'react'
import { makeStyles } from '@material-ui/core/styles'
import InputLabel from '@material-ui/core/InputLabel'
import MenuItem from '@material-ui/core/MenuItem'
import FormHelperText from '@material-ui/core/FormHelperText'
import FormControl from '@material-ui/core/FormControl'
import Select from '@material-ui/core/Select'
const useStyles = makeStyles((theme) => {
return {
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}
})
export default function SimpleSelect () {
const classes = useStyles()
const [age, setAge] = React.useState('')
const inputLabel = React.useRef(null)
const [labelWidth, setLabelWidth] = React.useState(0)
React.useEffect(() => {
setLabelWidth(inputLabel.current.offsetWidth)
}, [])
const handleChange = (event) => {
setAge(event.target.value)
}
return (
<div>
<FormControl className={classes.formControl}>
<InputLabel id="demo-simple-select-label">Age</InputLabel>
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={age}
onChange={handleChange}
>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel id="demo-simple-select-helper-label">Age</InputLabel>
<Select
labelId="demo-simple-select-helper-label"
id="demo-simple-select-helper"
value={age}
onChange={handleChange}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Some important helper text</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<Select
value={age}
onChange={handleChange}
displayEmpty
className={classes.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Without label</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel shrink id="demo-simple-select-placeholder-label-label">
Age
</InputLabel>
<Select
labelId="demo-simple-select-placeholder-label-label"
id="demo-simple-select-placeholder-label"
value={age}
onChange={handleChange}
displayEmpty
className={classes.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Label + placeholder</FormHelperText>
</FormControl>
<FormControl className={classes.formControl} disabled>
<InputLabel id="demo-simple-select-disabled-label">Name</InputLabel>
<Select
labelId="demo-simple-select-disabled-label"
id="demo-simple-select-disabled"
value={age}
onChange={handleChange}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Disabled</FormHelperText>
</FormControl>
<FormControl className={classes.formControl} error>
<InputLabel id="demo-simple-select-error-label">Name</InputLabel>
<Select
labelId="demo-simple-select-error-label"
id="demo-simple-select-error"
value={age}
onChange={handleChange}
renderValue={(value) => `⚠️ - ${value}`}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Error</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel id="demo-simple-select-readonly-label">Name</InputLabel>
<Select
labelId="demo-simple-select-readonly-label"
id="demo-simple-select-readonly"
value={age}
onChange={handleChange}
inputProps={{ readOnly: true }}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Read only</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<InputLabel id="demo-simple-select-autowidth-label">Age</InputLabel>
<Select
labelId="demo-simple-select-autowidth-label"
id="demo-simple-select-autowidth"
value={age}
onChange={handleChange}
autoWidth
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Auto width</FormHelperText>
</FormControl>
<FormControl className={classes.formControl}>
<Select
value={age}
onChange={handleChange}
displayEmpty
className={classes.selectEmpty}
>
<MenuItem value="" disabled>
Placeholder
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Placeholder</FormHelperText>
</FormControl>
<FormControl required className={classes.formControl}>
<InputLabel id="demo-simple-select-required-label">Age</InputLabel>
<Select
labelId="demo-simple-select-required-label"
id="demo-simple-select-required"
value={age}
onChange={handleChange}
className={classes.selectEmpty}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
<FormHelperText>Required</FormHelperText>
</FormControl>
<FormControl variant="outlined" className={classes.formControl}>
<InputLabel ref={inputLabel} id="demo-simple-select-outlined-label">
Age
</InputLabel>
<Select
labelId="demo-simple-select-outlined-label"
id="demo-simple-select-outlined"
value={age}
onChange={handleChange}
labelWidth={labelWidth}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
<FormControl variant="filled" className={classes.formControl}>
<InputLabel id="demo-simple-select-filled-label">Age</InputLabel>
<Select
labelId="demo-simple-select-filled-label"
id="demo-simple-select-filled"
value={age}
onChange={handleChange}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
<MenuItem value={10}>Ten</MenuItem>
<MenuItem value={20}>Twenty</MenuItem>
<MenuItem value={30}>Thirty</MenuItem>
</Select>
</FormControl>
</div>
)
}
it('renders selects', () => {
cy.viewport(1200, 600)
mount(<SimpleSelect />, {
stylesheets: [
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap',
'https://fonts.googleapis.com/icon?family=Material+Icons',
],
})
cy.get('#demo-simple-select').click()
cy.contains('[role=option]', 'Twenty')
.should('be.visible')
.click()
// check that other select has changed
cy.contains('#demo-simple-select-outlined', 'Twenty').should('be.visible')
})
@@ -1,62 +0,0 @@
/// <reference types="cypress" />
import { mount } from '@cypress/react'
// select example from
// https://material-ui.com/components/rating/
import React from 'react'
import Rating from '@material-ui/lab/Rating'
import Typography from '@material-ui/core/Typography'
import Box from '@material-ui/core/Box'
export default function SimpleRating ({ onSetRating }) {
const [value, setValue] = React.useState(2)
return (
<div>
<Box component="fieldset" mb={3} borderColor="transparent">
<Typography component="legend">Controlled</Typography>
<Rating
name="simple-controlled"
value={value}
onChange={(event, newValue) => {
setValue(newValue)
console.log('new value', newValue)
if (onSetRating) {
onSetRating(newValue)
}
}}
/>
</Box>
<Box component="fieldset" mb={3} borderColor="transparent">
<Typography component="legend">Read only</Typography>
<Rating name="read-only" value={value} readOnly />
</Box>
<Box component="fieldset" mb={3} borderColor="transparent">
<Typography component="legend">Disabled</Typography>
<Rating name="disabled" value={value} disabled />
</Box>
<Box component="fieldset" mb={3} borderColor="transparent">
<Typography component="legend">Pristine</Typography>
<Rating name="pristine" value={null} />
</Box>
</div>
)
}
it('renders simple rating', () => {
cy.viewport(300, 400)
const onSetRating = cy.stub()
mount(<SimpleRating onSetRating={onSetRating} />, {
stylesheets: [
'https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap',
'https://fonts.googleapis.com/icon?family=Material+Icons',
],
})
cy.get('label[for=simple-controlled-4]')
.click()
.then(() => {
expect(onSetRating).to.have.been.calledWith(4)
})
})
@@ -1,107 +0,0 @@
// Top 100 films as rated by IMDb users. http://www.imdb.com/chart/top
export const top100Films = [
{ title: 'The Shawshank Redemption', year: 1994 },
{ title: 'The Godfather', year: 1972 },
{ title: 'The Godfather: Part II', year: 1974 },
{ title: 'The Dark Knight', year: 2008 },
{ title: '12 Angry Men', year: 1957 },
{ title: 'Schindler\'s List', year: 1993 },
{ title: 'Pulp Fiction', year: 1994 },
{ title: 'The Lord of the Rings: The Return of the King', year: 2003 },
{ title: 'The Good, the Bad and the Ugly', year: 1966 },
{ title: 'Fight Club', year: 1999 },
{ title: 'The Lord of the Rings: The Fellowship of the Ring', year: 2001 },
{ title: 'Star Wars: Episode V - The Empire Strikes Back', year: 1980 },
{ title: 'Forrest Gump', year: 1994 },
{ title: 'Inception', year: 2010 },
{ title: 'The Lord of the Rings: The Two Towers', year: 2002 },
{ title: 'One Flew Over the Cuckoo\'s Nest', year: 1975 },
{ title: 'Goodfellas', year: 1990 },
{ title: 'The Matrix', year: 1999 },
{ title: 'Seven Samurai', year: 1954 },
{ title: 'Star Wars: Episode IV - A New Hope', year: 1977 },
{ title: 'City of God', year: 2002 },
{ title: 'Se7en', year: 1995 },
{ title: 'The Silence of the Lambs', year: 1991 },
{ title: 'It\'s a Wonderful Life', year: 1946 },
{ title: 'Life Is Beautiful', year: 1997 },
{ title: 'The Usual Suspects', year: 1995 },
{ title: 'Léon: The Professional', year: 1994 },
{ title: 'Spirited Away', year: 2001 },
{ title: 'Saving Private Ryan', year: 1998 },
{ title: 'Once Upon a Time in the West', year: 1968 },
{ title: 'American History X', year: 1998 },
{ title: 'Interstellar', year: 2014 },
{ title: 'Casablanca', year: 1942 },
{ title: 'City Lights', year: 1931 },
{ title: 'Psycho', year: 1960 },
{ title: 'The Green Mile', year: 1999 },
{ title: 'The Intouchables', year: 2011 },
{ title: 'Modern Times', year: 1936 },
{ title: 'Raiders of the Lost Ark', year: 1981 },
{ title: 'Rear Window', year: 1954 },
{ title: 'The Pianist', year: 2002 },
{ title: 'The Departed', year: 2006 },
{ title: 'Terminator 2: Judgment Day', year: 1991 },
{ title: 'Back to the Future', year: 1985 },
{ title: 'Whiplash', year: 2014 },
{ title: 'Gladiator', year: 2000 },
{ title: 'Memento', year: 2000 },
{ title: 'The Prestige', year: 2006 },
{ title: 'The Lion King', year: 1994 },
{ title: 'Apocalypse Now', year: 1979 },
{ title: 'Alien', year: 1979 },
{ title: 'Sunset Boulevard', year: 1950 },
{
title:
'Dr. Strangelove or: How I Learned to Stop Worrying and Love the Bomb',
year: 1964,
},
{ title: 'The Great Dictator', year: 1940 },
{ title: 'Cinema Paradiso', year: 1988 },
{ title: 'The Lives of Others', year: 2006 },
{ title: 'Grave of the Fireflies', year: 1988 },
{ title: 'Paths of Glory', year: 1957 },
{ title: 'Django Unchained', year: 2012 },
{ title: 'The Shining', year: 1980 },
{ title: 'WALL·E', year: 2008 },
{ title: 'American Beauty', year: 1999 },
{ title: 'The Dark Knight Rises', year: 2012 },
{ title: 'Princess Mononoke', year: 1997 },
{ title: 'Aliens', year: 1986 },
{ title: 'Oldboy', year: 2003 },
{ title: 'Once Upon a Time in America', year: 1984 },
{ title: 'Witness for the Prosecution', year: 1957 },
{ title: 'Das Boot', year: 1981 },
{ title: 'Citizen Kane', year: 1941 },
{ title: 'North by Northwest', year: 1959 },
{ title: 'Vertigo', year: 1958 },
{ title: 'Star Wars: Episode VI - Return of the Jedi', year: 1983 },
{ title: 'Reservoir Dogs', year: 1992 },
{ title: 'Braveheart', year: 1995 },
{ title: 'M', year: 1931 },
{ title: 'Requiem for a Dream', year: 2000 },
{ title: 'Amélie', year: 2001 },
{ title: 'A Clockwork Orange', year: 1971 },
{ title: 'Like Stars on Earth', year: 2007 },
{ title: 'Taxi Driver', year: 1976 },
{ title: 'Lawrence of Arabia', year: 1962 },
{ title: 'Double Indemnity', year: 1944 },
{ title: 'Eternal Sunshine of the Spotless Mind', year: 2004 },
{ title: 'Amadeus', year: 1984 },
{ title: 'To Kill a Mockingbird', year: 1962 },
{ title: 'Toy Story 3', year: 2010 },
{ title: 'Logan', year: 2017 },
{ title: 'Full Metal Jacket', year: 1987 },
{ title: 'Dangal', year: 2016 },
{ title: 'The Sting', year: 1973 },
{ title: '2001: A Space Odyssey', year: 1968 },
{ title: 'Singin\' in the Rain', year: 1952 },
{ title: 'Toy Story', year: 1995 },
{ title: 'Bicycle Thieves', year: 1948 },
{ title: 'The Kid', year: 1921 },
{ title: 'Inglourious Basterds', year: 2009 },
{ title: 'Snatch', year: 2000 },
{ title: '3 Idiots', year: 2009 },
{ title: 'Monty Python and the Holy Grail', year: 1975 },
]
@@ -1,7 +0,0 @@
# MobX v6 example
Based on the example from [the docs](https://mobx.js.org/react-integration.html)
See [Timer.js](Timer.js) that has an observable class, [timer-view.jsx](timer-view.jsx) for React component linked to a timer instance, and [timer-spec.jsx](timer-spec.jsx) showing a component test.
![Timer spec](images/timer.png)
@@ -1,13 +0,0 @@
import { makeAutoObservable } from 'mobx'
export class Timer {
secondsPassed = 0
constructor () {
makeAutoObservable(this)
}
increaseTimer () {
this.secondsPassed += 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

@@ -1,8 +0,0 @@
import React from 'react'
import { observer } from 'mobx-react-lite'
// A function component wrapped with `observer` will react
// to any future change in an observable it used before.
export const TimerView = observer(({ timer }) => (
<span>Seconds passed: {timer.secondsPassed}</span>
))
@@ -1,39 +0,0 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Timer } from './Timer'
import { TimerView } from './timer-view'
describe('MobX v6', () => {
context('TimerView', () => {
it('increments every second', () => {
const myTimer = new Timer()
mount(<TimerView timer={myTimer} />)
cy.contains('Seconds passed: 0').then(() => {
// we can increment the timer from outside
myTimer.increaseTimer()
})
cy.contains('Seconds passed: 1')
// by wrapping the timer and giving it an alias
cy.wrap(myTimer).as('timer')
// we can "insert" it into the command chain
// using cy.get() and then invoke methods
// as if every command was inside .then(() => {...}) callback
cy.get('@timer').invoke('increaseTimer')
cy.contains('Seconds passed: 2')
cy.get('@timer').invoke('increaseTimer')
cy.contains('Seconds passed: 3')
cy.get('@timer').invoke('increaseTimer')
cy.contains('Seconds passed: 4')
// we can also ask the timer for the current value
cy.get('@timer').invoke('increaseTimer')
cy.get('@timer')
.its('secondsPassed')
.should('equal', 5)
})
})
})
@@ -2,8 +2,7 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Users } from './1-users.jsx'
// to mock CommonJS module loaded from `node_modules` use "require" in spec file
const Axios = require('axios')
import axios from 'axios'
describe('Mocking Axios', () => {
it('shows real users', () => {
@@ -12,8 +11,9 @@ describe('Mocking Axios', () => {
})
// https://github.com/bahmutov/@cypress/react/issues/338
it('mocks axios.get', () => {
cy.stub(Axios, 'get')
// TODO: Support stubbing ES Modules. The above hack won't work with Vite.
it.skip('mocks axios.get', () => {
cy.stub(axios, 'get')
.resolves({
data: [
{
@@ -2,8 +2,7 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Users } from './2-users-named.jsx'
// to mock CommonJS module loaded from `node_modules` use "require" in spec file
const Axios = require('axios')
import axios from 'axios'
describe('Mocking Axios named import get', () => {
it('shows real users', () => {
@@ -11,9 +10,9 @@ describe('Mocking Axios named import get', () => {
cy.get('li').should('have.length', 3)
})
it('mocks get', () => {
console.log('Axios', Axios)
cy.stub(Axios, 'get')
// TODO: Support stubbing ES Modules
it.skip('mocks get', () => {
cy.stub(axios, 'get')
.resolves({
data: [
{
@@ -2,7 +2,7 @@
import React from 'react'
import { mount } from '@cypress/react'
import { Users } from './3-users-api.jsx'
import * as Axios from './axios-api'
import * as axios from './axios-api'
describe('Mocking wrapped Axios', () => {
it('shows real users', () => {
@@ -10,9 +10,9 @@ describe('Mocking wrapped Axios', () => {
cy.get('li').should('have.length', 3)
})
it('mocks get', () => {
console.log('Axios', Axios)
cy.stub(Axios, 'get')
// TODO: Support stubbing ES Modules
it.skip('mocks get', () => {
cy.stub(axios, 'get')
.resolves({
data: [
{
@@ -1,6 +1,6 @@
import React from 'react'
// import wrapped Axios method
import { get } from './axios-api'
import axiosApi from './axios-api'
export class Users extends React.Component {
constructor (props) {
@@ -11,7 +11,8 @@ export class Users extends React.Component {
}
componentDidMount () {
get('https://jsonplaceholder.cypress.io/users?_limit=3').then((response) => {
console.log({ axiosApi })
axiosApi.get('https://jsonplaceholder.cypress.io/users?_limit=3').then((response) => {
// JSON responses are automatically parsed.
this.setState({
users: response.data,
@@ -1,2 +0,0 @@
// wrap Axios exports
export * from 'axios'
@@ -0,0 +1,4 @@
// wrap Axios exports
import axios from 'axios'
export default axios
@@ -1,47 +0,0 @@
# Mocking ES6 imports
The original example comes from https://reactjs.org/docs/testing-recipes.html#mocking-modules
The [contact.js](contact.js) component imports [map.js](map.js) component. But the real Map is expensive to render - it uses Google Maps, etc. Thus during tests we would like to replace the real Map with `DummyMap` component that only renders the props.
See [spec.js](spec.js) test file. The recommended approach is to mock the ES6 import.
```js
// contact.js
import Map from './map'
export default function Contact(props) {
// renders <Map ...>
}
// spec.js
import Contact from './contact'
import * as MapModule from './map'
const DummyMap = props => (
<div data-testid="map">
DummyMap {props.center.lat}:{props.center.long}
</div>
)
it('renders stubbed Map', () => {
// DummyMap component will be called with props and any other arguments
cy.stub(MapModule, 'default').callsFake(DummyMap)
cy.viewport(500, 500)
const center = { lat: 0, long: 0 }
mount(
<Contact
name="Joni Baez"
email="test@example.com"
site="http://test.com"
center={center}
/>,
)
cy.contains('Contact Joni Baez via')
// confirm DummyMap renders "0:0" passed via props
cy.contains('[data-testid="map"]', '0:0').should('be.visible')
})
```
![Dummy map test](images/dummy-map.png)
@@ -1,22 +0,0 @@
// example from https://reactjs.org/docs/testing-recipes.html#mocking-modules
import React from 'react'
import Map from './map'
export default function Contact (props) {
return (
<div>
<address>
Contact {props.name} via{' '}
<a data-testid="email" href={`mailto:${props.email}`}>
email
</a>{' '}
or on their{' '}
<a data-testid="site" href={props.site}>
website
</a>
.
</address>
<Map center={props.center} />
</div>
)
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

@@ -1,18 +0,0 @@
import React from 'react'
import { GoogleMap, withGoogleMap, withScriptjs } from 'react-google-maps'
const GMap = withScriptjs(
withGoogleMap((props) => <GoogleMap id="example-map" center={props.center} />),
)
export default function Map (props) {
return (
<GMap
googleMapURL="https://maps.googleapis.com/maps/api/js?key=AIzaSyC4R6AN7SmujjPUIGKdyao2Kqitzr1kiRg&v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `400px` }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
}
@@ -1,42 +0,0 @@
import React from 'react'
import { mount } from '@cypress/react'
// Component "Contact" has child component "Map" that is expensive to render
import Contact from './contact'
import * as MapModule from './map'
describe('Mock imported component', () => {
// mock Map component used by Contact component
// whenever React tries to instantiate using Map constructor
// call DummyMap constructor
const DummyMap = (props) => {
return (
<div data-testid="map">
DummyMap {props.center.lat}:{props.center.long}
</div>
)
}
context('via mocking ES6 default import', () => {
// recommended
it('renders stubbed Map', () => {
// DummyMap component will be called with props and any other arguments
cy.stub(MapModule, 'default').callsFake(DummyMap)
cy.viewport(500, 500)
const center = { lat: 0, long: 0 }
mount(
<Contact
name="Joni Baez"
email="test@example.com"
site="http://test.com"
center={center}
/>,
)
cy.contains('Contact Joni Baez via')
// confirm DummyMap renders "0:0" passed via props
cy.contains('[data-testid="map"]', '0:0').should('be.visible')
})
})
})
@@ -6,7 +6,8 @@ import * as services from './services'
const ingredients = ['bacon', 'tomato', 'mozzarella', 'pineapples']
describe('RemotePizza', () => {
// TODO: Support stubbing ES Modules
describe.skip('RemotePizza', () => {
it('mocks named import from services', () => {
cy.stub(services, 'fetchIngredients')
.resolves({ args: { ingredients } })
@@ -10,7 +10,8 @@ describe('Mocking ES6 import', () => {
cy.contains('h1', 'real greeting').should('be.visible')
})
it('shows mock greeting', () => {
// TODO: Stub support for ES Modules in Vite.
it.skip('shows mock greeting', () => {
// stubbing ES6 named imports works via
// @babel/plugin-transform-modules-commonjs with "loose: true"
// because the generated properties are configurable
@@ -1,9 +0,0 @@
# radioactive-state example
Testing components that use [radioactive-state](https://github.com/MananTank/radioactive-state) library.
![Counters test](images/counters.gif)
![Todos test](images/todos.gif)
Tests [counter](./counter), [counters](./counters), [todos](./todos)
@@ -1,20 +0,0 @@
import React from 'react'
import useRS from 'radioactive-state'
// click on the counter to increment
export const Counter = () => {
// create a radioactive state
const state = useRS({
count: 0,
})
// mutating the state triggers a re-render
const increment = () => state.count++
return (
<div className="count" onClick={increment}>
{state.count}
</div>
)
}
@@ -1,43 +0,0 @@
.App {
font-family: sans-serif;
text-align: center;
display: grid;
place-items: center;
min-height: 100vh;
}
.count {
font-size: 5rem;
color: white;
width: 150px;
height: 150px;
border-radius: 50%;
display: grid;
place-items: center;
cursor: pointer;
user-select: none;
transition: all 100ms ease;
background: #3069f9;
border: 10px solid rgb(198, 214, 253);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
}
.count:active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@@ -1,21 +0,0 @@
import './counter.css'
import React from 'react'
import { mount } from '@cypress/react'
import { Counter } from './Counter.jsx'
describe('reactive-state Counter', () => {
it('increments count on click', () => {
mount(
<div className="App">
<Counter />
</div>,
)
cy.contains('.count', '0')
.click()
.click()
.click()
cy.contains('.count', '3')
})
})
@@ -1,33 +0,0 @@
import React from 'react'
import useRS from 'radioactive-state'
// deep mutation also triggers re-render !
const Counters = () => {
const state = useRS({
counts: [0],
sum: 0,
})
const increment = (i) => {
state.counts[i]++
state.sum++
}
const addCounter = () => state.counts.push(0)
return (
<>
<button onClick={addCounter}> Add Counter </button>
<div className="counts">
{state.counts.map((count, i) => (
<div className="count" onClick={() => increment(i)} key={i}>
{count}
</div>
))}
</div>
<div className="count sum">{state.sum}</div>
</>
)
}
export default Counters
@@ -1,77 +0,0 @@
.App {
font-family: sans-serif;
text-align: center;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
.count {
background: #3069f9;
border: 6px solid rgb(198, 214, 253);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
margin: 10px;
font-size: 2rem;
color: white;
width: 80px;
height: 80px;
border-radius: 50%;
display: grid;
place-items: center;
cursor: pointer;
user-select: none;
transition: all 100ms ease;
}
.count.sum {
background: #ff3944;
border-color: #ffb0b5;
width: 100px;
height: 100px;
font-size: 3rem;
}
.counts {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.count:not(.sum):active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
background: #ffd400;
border: 5px solid #fdee81;
outline: none;
font-size: 1.5rem;
padding: 15px;
border-radius: 10px;
margin: 20px;
transition: all 150ms ease;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
button:active {
transform: scale(0.9);
}
@@ -1,42 +0,0 @@
import './counters.css'
import React from 'react'
import { mount } from '@cypress/react'
import Counters from './Counters.jsx'
describe('reactive-state Counters', () => {
it('increments single count on click', () => {
mount(
<div className="App">
<Counters />
</div>,
)
cy.contains('.count', '0')
.click()
.click()
.click()
// increments the counter itself
cy.contains('.count', '3')
// increments the sum
cy.contains('.sum', '3')
// add two more counters
cy.contains('Add Counter')
.click()
.click()
cy.get('.counts .count').should('have.length', 3)
// clicking the new counters increments the sum
cy.get('.count')
.eq(1)
.click()
cy.contains('.sum', '4')
cy.get('.count')
.eq(2)
.click()
cy.contains('.sum', '5')
})
})
Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

@@ -1,29 +0,0 @@
import React from 'react'
import useRS from 'radioactive-state'
const AddTodo = ({ onAdd }) => {
const state = useRS({
input: '',
})
// handle events
const handleEnter = (e) => e.key === 'Enter' && handleAdd()
const handleAdd = () => {
if (!state.input) return // ignore empty input
const todo = { title: state.input, completed: false } // make todo
onAdd(todo) // add it
state.input = '' // clear input
}
return (
<div className="add-todo">
<input {...state.$input} onKeyDown={handleEnter} />
<button onClick={handleAdd}> Add </button>
</div>
)
}
export default AddTodo
@@ -1,24 +0,0 @@
import React from 'react'
// todo is a reactive prop,
// mutating it triggers re-render in parent component: Todos
const Todo = ({ todo, onRemove }) => {
const className = todo.completed ? 'todo completed' : 'todo'
const toggleTodo = () => {
todo.completed = !todo.completed // 🤩
}
return (
<div className={className}>
<div className="todo_title" onClick={toggleTodo}>
{todo.title}
</div>
<div className="todo_remove" onClick={onRemove}>
x
</div>
</div>
)
}
export default Todo
@@ -1,31 +0,0 @@
import React from 'react'
import useRS from 'radioactive-state'
import Todo from './Todo'
import AddTodo from './AddTodo'
const Todos = () => {
const state = useRS({
todos: [],
})
const removeTodo = (i) => state.todos.splice(i, 1)
const addTodo = (todo) => state.todos.push(todo)
return (
<div className="container">
<AddTodo onAdd={addTodo} />
{!state.todos.length && <div className="no-todos"> No Todos added </div>}
<div className="todos">
{state.todos.map((todo, i) => {
return (
<Todo todo={todo} key={i} onRemove={() => removeTodo(i)} />
)
})}
</div>
</div>
)
}
export default Todos
@@ -1,98 +0,0 @@
.App {
font-family: Arial, sans-serif;
max-width: 420px;
margin: 0 auto;
font-size: 1.5rem;
padding: 30px;
border-radius: 8px;
background: white;
line-height: 1;
}
body {
padding: 50px;
}
.add-todo {
display: flex;
width: 100%;
border-radius: 6px;
overflow: hidden;
border: 4px solid #313d52;
margin-bottom: 2rem;
}
.todos {
margin-top: 20px;
}
.todo {
padding-right: 1em;
cursor: pointer;
background: #eef1f6;
border-radius: 0.5rem;
margin-bottom: 1em;
font-size: 1.2rem;
box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
color: #313d52;
}
.todo_title {
flex-grow: 1;
padding: 1em;
}
.todo_remove {
background: #dde3ed;
width: 2em;
height: 2em;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
color: #6d7e9b;
}
.completed {
color: #a0aec0;
}
.completed .todo_title {
text-decoration: line-through;
}
button,
input {
display: block;
border: none;
overflow: hidden;
font-size: 1.2rem;
}
input {
flex-grow: 1;
padding: 1rem;
line-height: 1;
display: block;
color: #313d52;
}
button {
padding: 1rem;
flex-shrink: 0;
background: #313d52;
border: none;
color: white;
}
.no-todos {
color: #a0aec0;
font-size: 2rem;
}
* {
box-sizing: border-box;
}
@@ -1,35 +0,0 @@
import './todos.css'
import React from 'react'
import { mount } from '@cypress/react'
import Todos from './Todos'
describe('reactive-state Todos', () => {
it('adds and removes todos', () => {
mount(
<div className="App">
<Todos />
</div>,
)
cy.get('.add-todo input').type('code{enter}')
cy.get('.add-todo input').type('test')
cy.get('.add-todo')
.contains('Add')
.click()
// now check things
cy.get('.todos .todo').should('have.length', 2)
// remove the first one
cy.get('.todos .todo')
.first()
.should('contain', 'code')
.find('.todo_remove')
.click()
// single todo left
cy.get('.todos .todo')
.should('have.length', 1)
.first()
.should('contain', 'test')
})
})
@@ -1,15 +0,0 @@
App and tests modeled from https://github.com/softchris/react-book/tree/7bd767bb39f59977b107d07f383a8f4e32a12857/Testing/test-demo for https://softchris.github.io/books/react/
## Selecting React Components
Typically we suggest selecting DOM elements using public properties likes data attributes, labels, text, CSS class names, or ids. If you really want to select React components using props or state values, combine `@cypress/react` with [cypress-react-selector](https://github.com/abhinaba-ghosh/cypress-react-selector) plugin.
See file [./src/components/ProductsList.spec.js](./src/components/ProductsList.spec.js) for example.
```js
// find a single instance with prop
// <AProduct name={'Second item'} />
cy.react('AProduct', { name: 'Second item' })
.should('be.visible')
.and('have.text', 'Second item')
```
@@ -1,32 +0,0 @@
.App {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 80px;
}
.App-header {
background-color: #222;
height: 150px;
padding: 20px;
color: white;
}
.App-title {
font-size: 1.5em;
}
.App-intro {
font-size: large;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -1,24 +0,0 @@
import React, { Component } from 'react'
import logo from './logo.svg'
import './App.css'
import Todos from './Todos'
import Select from './Select'
class App extends Component {
render () {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<div className="App-intro">
<Todos todos={[{ title: 'test' }]} />
<Select />
</div>
</div>
)
}
}
export default App
@@ -1,25 +0,0 @@
/// <reference types="cypress" />
import { mount } from '@cypress/react'
import React from 'react'
import Select from './Note'
describe('Note', () => {
it('save text', () => {
mount(<Select />)
cy.get('#change').type('input text')
cy.contains('button', 'Save').click()
cy.get('[data-testid=saved]').should('have.text', 'Saved: input text')
})
it('load data', () => {
mount(<Select />)
cy.contains('button', 'Load').click()
// there is a built-in delay in loading the data
// but we don't worry about it - we just check if the text eventually appears
cy.get('[data-testid=item]')
.should('have.length', 2)
.and('be.visible')
.first()
.should('have.text', 'test')
})
})
@@ -1,59 +0,0 @@
import React from 'react'
class Note extends React.Component {
state = {
content: '',
saved: '',
}
onChange = (evt) => {
this.setState({
content: evt.target.value,
})
console.log('updating content')
}
save = () => {
this.setState({
saved: `Saved: ${this.state.content}`,
})
}
load = () => {
let me = this
setTimeout(() => {
me.setState({
data: [{ title: 'test' }, { title: 'test2' }],
})
}, 3000)
}
render () {
return (
<React.Fragment>
<label htmlFor="change">Change text</label>
<input id="change" placeholder="change text" onChange={this.onChange} />
<div data-testid="saved">{this.state.saved}</div>
{this.state.data && (
<div data-testid="data">
{this.state.data.map((item) => {
return (
<div data-testid="item" className="item">
{item.title}
</div>
)
})}
</div>
)}
<div>
<button onClick={this.save}>Save</button>
<button onClick={this.load}>Load</button>
</div>
</React.Fragment>
)
}
}
export default Note
@@ -1,3 +0,0 @@
.selected {
background: green;
}

Some files were not shown because too many files have changed in this diff Show More