├── .github
└── workflows
│ └── nodejs.yml
├── .gitignore
├── .huskyrc
├── .npmignore
├── LICENSE
├── README.md
├── babel.config.js
├── build
├── build.js
└── configs.js
├── commitlint.config.js
├── dont-cleanup-after-each.js
├── jest-preset.js
├── jest.config.js
├── package.json
├── src
├── __tests__
│ ├── __snapshots__
│ │ ├── functional-component.test.tsx.snap
│ │ └── snapshot-component.test.tsx.snap
│ ├── class-component.test.tsx
│ ├── functional-component-effect.test.tsx
│ ├── functional-component.test.tsx
│ └── snapshot-component.test.tsx
└── index.ts
├── test
└── unit
│ ├── .eslintrc
│ └── setup.js
├── tsconfig.json
├── typings.d.ts
└── yarn.lock
/.github/workflows/nodejs.yml:
--------------------------------------------------------------------------------
1 | name: Node CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | strategy:
11 | matrix:
12 | node-version: [10.x]
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Use Node.js ${{ matrix.node-version }}
17 | uses: actions/setup-node@v1
18 | with:
19 | node-version: ${{ matrix.node-version }}
20 | - name: npm install, build, and test
21 | run: |
22 | npm install
23 | npm run build
24 | npm run test
25 | env:
26 | CI: true
27 | - name: Upload coverage to Codecov
28 | uses: codecov/codecov-action@v1.0.2
29 | with:
30 | token: ${{secrets.CODECOV_TOKEN}}
31 | - name: Release
32 | uses: codfish/semantic-release-action@master
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | .idea
8 |
9 | # dist
10 | lib
11 | dist
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | lib-cov
20 | test/unit/coverage/
21 | test-report.xml
22 |
23 | node_modules/
24 | .yarn-integrity
25 | .DS_Store
26 | .cache/
27 | .puppet-master/
28 | storybook-static/
29 |
--------------------------------------------------------------------------------
/.huskyrc:
--------------------------------------------------------------------------------
1 | {
2 | "hooks": {
3 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
4 | "pre-push": "npm test",
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | TODO.md
4 | *.log
5 | .idea
6 | src
7 | .babelrc
8 | .huskyrc
9 | .travis.yml
10 | commitlint.config.js
11 | .storybook
12 | test/unit/coverage/
13 | test-report.xml
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 ThoughtWorks China
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # taro-testing-library
2 | Simple and complete taro.js 2.x testing utilities that encourage good testing practices.
3 |
4 | [](https://github.com/ThoughtWorksChina/taro-testing-library/actions)
5 | [](https://www.npmjs.com/package/taro-testing-library)
6 | [](https://github.com/ThoughtWorksChina/taro-testing-library/blob/master/LICENSE)
7 | [](https://codecov.io/gh/ThoughtWorksChina/taro-testing-library)
8 |
9 |
10 | ## Install
11 | ```bash
12 | // use yarn
13 | yarn add taro-testing-library -D
14 | // use npm
15 | npm install taro-testing-library -D
16 | ```
17 |
18 | ### For Taro 3.x
19 |
20 | For Taro 3.x and React, can use [react-testing-library](https://github.com/testing-library/react-testing-library) directly.
21 |
22 | the only need is to configure `moduleNameMapper` in `jest.config.js` like this.
23 |
24 | ```js
25 | {
26 | moduleNameMapper: {
27 | '@tarojs/components': '@tarojs/components/dist-h5/react',
28 | ...
29 | },
30 | }
31 | ```
32 |
33 |
34 | ## Usage
35 | set `preset` in your jest config file
36 | ```json
37 | {
38 | "preset": "taro-testing-library"
39 | }
40 | ```
41 |
42 | ### API
43 |
44 |
45 | ## API
46 | ### render
47 | * `render(Component, { container, target }) => { container, unmount, rerender }`: render method to mount a component
48 |
49 | * `container`: The HTML element the component is mounted into.
50 |
51 | default : `document.body`
52 | * `target`: The HTML element the component is mounted.
53 |
54 | default : `container.appendChild(document.createElement('div'))`
55 |
56 | #### Result
57 | * `container`: container
58 | * `component`: created Taro.js component
59 | * `rerender(Component)`: method of rerender component
60 | * `unmount()`: method of unmount component
61 | * `debug()`: method of log current dom
62 | * `...queries`: Returns all [query functions](https://testing-library.com/docs/dom-testing-library/api-queries) that are binded to the target.
63 |
64 |
65 | ### renderToString
66 | `renderToString(Component) => string`: export from [nerv-server](https://github.com/NervJS/nerv-server), you can use it to match snapshot
67 |
68 | ### cleanup
69 | Unmounts the component from the container and destroys the container.
70 |
71 | `cleanup()` is called after each test automatically by default if the testing framework you're using supports the afterEach global (like mocha, Jest, and Jasmine).
72 |
73 | However, you may choose to skip the auto cleanup by setting the `TTL_SKIP_AUTO_CLEANUP` env variable to 'true'.
74 |
75 | To make this even easier, you can also simply import `taro-testing-library/dont-cleanup-after-each` which will do the same thing.
76 |
77 | ## Demo
78 |
79 | ### Component
80 |
81 | ```jsx
82 | import Taro, { useState } from '@tarojs/taro';
83 | import { Text } from '@tarojs/components';
84 |
85 | const Counter = (props) => {
86 | const { initial = 1 } = props;
87 | const [count, setCount] = useState(initial)
88 | return (
89 | {setCount(count+1)}} className="number">
90 | {count}
91 |
92 | );
93 | };
94 | ```
95 |
96 | ### Test
97 |
98 | ```jsx
99 | import Taro from '@tarojs/taro';
100 | import { act, render } from 'taro-testing-library';
101 |
102 | test('should render component', () => {
103 | const { container } = render();
104 | const $number = container.querySelector('.number');
105 | expect($number.innerHTML).toEqual('1');
106 | });
107 |
108 | test('should rerender when trigger setState hooks', () => {
109 | const { container } = render();
110 | const $number = container.querySelector('.number');
111 | act(() => {
112 | $number.click()
113 | })
114 | expect($number.innerHTML).toEqual(`2`);
115 | });
116 |
117 | it('should support snapshot', () => {
118 | const component = renderToString(
component without state
);
119 | expect(component).toMatchSnapshot();
120 | })
121 | ```
122 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | const apis = require("@tarojs/taro-h5/dist/taroApis");
2 |
3 | module.exports = {
4 | presets: [
5 | [
6 | "@babel/env",
7 | {
8 | spec: true,
9 | useBuiltIns: false
10 | }
11 | ],
12 | "@babel/preset-typescript"
13 | ],
14 | plugins: [
15 | "@babel/plugin-proposal-class-properties",
16 | [
17 | "@babel/plugin-transform-react-jsx",
18 | {
19 | pragma: "Nerv.createElement"
20 | }
21 | ],
22 | ["@babel/plugin-proposal-object-rest-spread"],
23 | [
24 | "babel-plugin-transform-taroapi",
25 | {
26 | apis,
27 | packageName: "@tarojs/taro-h5"
28 | }
29 | ]
30 | ]
31 | };
32 |
--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 | const zlib = require('zlib')
4 | const rollup = require('rollup')
5 | const configs = require('./configs')
6 |
7 | if (!fs.existsSync('dist')) {
8 | fs.mkdirSync('dist')
9 | }
10 |
11 | build(Object.keys(configs).map(key => configs[key]))
12 |
13 |
14 | function build (builds) {
15 | let built = 0
16 | const total = builds.length
17 | const next = () => {
18 | buildEntry(builds[built]).then(() => {
19 | built++
20 | if (built < total) {
21 | next()
22 | }
23 | }).catch(logError)
24 | }
25 |
26 | next()
27 | }
28 |
29 |
30 | async function buildEntry({ input, output }) {
31 | // create a bundle
32 | const bundle = await rollup.rollup(input)
33 | await bundle.write(output);
34 | const res = await bundle.generate(output)
35 | await write(output.file, res.output[0].code)
36 | }
37 |
38 | function write (dest, code, zip) {
39 | return new Promise((resolve, reject) => {
40 | function report (extra) {
41 | console.log(blue(path.relative(process.cwd(), dest)) + ' ' + getSize(code) + (extra || ''))
42 | resolve()
43 | }
44 | fs.writeFile(dest, code, err => {
45 | if (err) return reject(err)
46 | zlib.gzip(code, (err, zipped) => {
47 | if (err) return reject(err)
48 | report(' (gzipped: ' + getSize(zipped) + ')')
49 | })
50 | })
51 | })
52 | }
53 |
54 | function getSize (code) {
55 | return (code.length / 1024).toFixed(2) + 'kb'
56 | }
57 |
58 | function logError (e) {
59 | console.log(e)
60 | }
61 |
62 | function blue (str) {
63 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
64 | }
--------------------------------------------------------------------------------
/build/configs.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const typescript = require('rollup-plugin-typescript2')
3 | const commonjs = require('rollup-plugin-commonjs')
4 | const { uglify } = require('rollup-plugin-uglify')
5 | const { terser } = require('rollup-plugin-terser')
6 | const Nerv = require('nervjs')
7 | const pkg = require('../package.json')
8 |
9 | const resolve = _path => path.resolve(__dirname, '../', _path)
10 |
11 | const configs = {
12 | cjs: {
13 | input: resolve('src/index.ts'),
14 | file: pkg.main,
15 | format: 'cjs',
16 | env: 'production'
17 | },
18 | es: {
19 | input: resolve('src/index.ts'),
20 | file: pkg.module,
21 | format: 'es',
22 | env: 'production'
23 | },
24 | }
25 |
26 | const compressPlugins = {
27 | cjs: uglify,
28 | es: terser
29 | }
30 |
31 | function genConfig (opts) {
32 | const config = {
33 | input: {
34 | input: opts.input,
35 | plugins: [
36 | commonjs({
37 | include: 'node_modules/**',
38 | namedExports: {
39 | nervjs: Object.keys(Nerv),
40 | }
41 | }),
42 | typescript(),
43 | ],
44 | external: [
45 | 'nervjs',
46 | ],
47 | },
48 | output: {
49 | file: opts.file,
50 | format: opts.format,
51 | exports: 'named',
52 | }
53 | }
54 | const method = compressPlugins[opts.format]
55 | if ( opts.env === 'production' && method ) {
56 | config.input.plugins.push(
57 | method()
58 | )
59 | }
60 | return config
61 | }
62 |
63 | function mapValues (obj, fn) {
64 | const res = {}
65 | Object.keys(obj).forEach(key => {
66 | res[key] = fn(obj[key], key)
67 | })
68 | return res
69 | }
70 |
71 | module.exports = mapValues(configs, genConfig)
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {extends: ['@commitlint/config-conventional']}
2 |
--------------------------------------------------------------------------------
/dont-cleanup-after-each.js:
--------------------------------------------------------------------------------
1 | process.env.TTL_SKIP_AUTO_CLEANUP = true
--------------------------------------------------------------------------------
/jest-preset.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-commonjs
2 | module.exports = {
3 | moduleNameMapper: {
4 | 'react': 'nervjs',
5 | 'react-addons-test-utils': 'nerv-test-utils',
6 | 'create-react-class': 'nerv-create-class',
7 | 'react-dom': 'nervjs',
8 | '@tarojs/taro': '@tarojs/taro-h5',
9 | 'weui': 'identity-obj-proxy',
10 | '\\.(css|less|sass|scss|styl)$': 'identity-obj-proxy',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: "./jest-preset.js",
3 | verbose: true,
4 | moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json"],
5 | rootDir: __dirname,
6 | testRegex: "(.*\\.(test|spec))\\.tsx?$",
7 | transform: {
8 | "^.+\\.tsx?$": "babel-jest"
9 | },
10 | transformIgnorePatterns: ["/node_modules/"],
11 | setupFiles: [
12 | "/test/unit/setup"
13 | ],
14 | collectCoverage: true,
15 | coverageReporters: ["html", "text-summary", "lcov"],
16 | coverageDirectory: "/test/unit/coverage",
17 | };
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "taro-testing-library",
3 | "version": "0.1.0",
4 | "description": "Simple and complete taro.js 2.x testing utilities that encourage good testing practices.",
5 | "main": "dist/index.js",
6 | "module": "dist/index.es.js",
7 | "types": "dist/index.d.ts",
8 | "scripts": {
9 | "build": "node ./build/build.js",
10 | "commit": "npx git-cz",
11 | "test": "jest --config jest.config.js --no-cache"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/ThoughtWorksChina/taro-testing-library.git"
16 | },
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/ThoughtWorksChina/taro-testing-library/issues"
21 | },
22 | "homepage": "https://github.com/ThoughtWorksChina/taro-testing-library#readme",
23 | "devDependencies": {
24 | "@babel/core": "^7.7.7",
25 | "@babel/plugin-proposal-class-properties": "^7.7.4",
26 | "@babel/plugin-proposal-decorators": "^7.8.3",
27 | "@babel/plugin-proposal-object-rest-spread": "^7.7.7",
28 | "@babel/plugin-transform-react-jsx": "^7.7.7",
29 | "@babel/preset-env": "^7.7.7",
30 | "@babel/preset-typescript": "^7.7.7",
31 | "@commitlint/config-conventional": "^8.3.4",
32 | "@rollup/plugin-typescript": "^2.1.0",
33 | "@tarojs/components": "^2.0.6",
34 | "@tarojs/taro": "^2.0.6",
35 | "@tarojs/taro-h5": "^2.0.6",
36 | "@types/jest": "^24.0.18",
37 | "babel-core": "^7.0.0-bridge.0",
38 | "babel-jest": "23.6.0",
39 | "babel-plugin-transform-taroapi": "^2.0.6",
40 | "commitlint": "^8.3.5",
41 | "husky": "^4.2.1",
42 | "jest": "23.6.0",
43 | "nerv-create-class": "^1.5.6",
44 | "nerv-test-utils": "^1.5.6",
45 | "nervjs": "^1.5.6",
46 | "rollup": "^1.23.1",
47 | "rollup-plugin-commonjs": "^10.1.0",
48 | "rollup-plugin-terser": "^5.1.2",
49 | "rollup-plugin-typescript2": "^0.24.3",
50 | "rollup-plugin-uglify": "^6.0.3",
51 | "typescript": "^3.6.3"
52 | },
53 | "peerDependencies": {
54 | "nervjs": "^1.5.0"
55 | },
56 | "dependencies": {
57 | "@testing-library/dom": "^6.15.0",
58 | "identity-obj-proxy": "^3.0.0",
59 | "nerv-server": "^1.5.7"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/functional-component.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`functional component test should support snapshot 1`] = `"component without state
"`;
4 |
--------------------------------------------------------------------------------
/src/__tests__/__snapshots__/snapshot-component.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`snapshot test should pass snapshot 1`] = `
4 |
5 |
8 | 1
9 |
10 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/__tests__/class-component.test.tsx:
--------------------------------------------------------------------------------
1 | import Taro, {Component} from '@tarojs/taro';
2 | import { act, render } from '../index';
3 | import {StandardProps} from "@tarojs/components/types/common";
4 | import {Text} from "@tarojs/components";
5 |
6 | interface CounterProps extends StandardProps {
7 | initial?: number;
8 | }
9 |
10 | interface CounterState {
11 | count: number;
12 | }
13 |
14 | class Counter extends Component {
15 |
16 | static defaultProps = {
17 | initial: 1
18 | }
19 |
20 | static externalClasses = ['custom-class'];
21 |
22 | constructor(props: CounterProps) {
23 | super(props);
24 | this.state = {
25 | count: props.initial as number
26 | }
27 | }
28 |
29 | render() {
30 | const { count } = this.state
31 | return (
32 | {this.setState({
34 | count: count + 1
35 | })}}
36 | className="number custom-class"
37 | >{count}
38 | )
39 | }
40 | }
41 |
42 | describe('class component test', () => {
43 | it('should render componen', () => {
44 | const { container } = render();
45 | const $number = container.querySelector('.number') as HTMLSpanElement;
46 | expect($number.innerHTML).toEqual('1');
47 | });
48 |
49 | it('should render component with props', () => {
50 | const initial = 10
51 | const { container } = render();
52 | const $number = container.querySelector('.number') as HTMLSpanElement;
53 | expect($number.innerHTML).toEqual(`${initial}`);
54 | });
55 |
56 | it('should rerender when trigger setState hooks', () => {
57 | const { container } = render();
58 | const $number = container.querySelector('.number') as HTMLSpanElement;
59 | act(() => {
60 | $number.click()
61 | })
62 | expect($number.innerHTML).toEqual(`2`);
63 | });
64 |
65 |
66 | it('should rerender when excute rerender methods', () => {
67 | const { container, rerender } = render();
68 | expect(container.querySelector('.number').innerHTML).toEqual("1");
69 | rerender()
70 | expect(container.querySelector('.number').innerHTML).toEqual("2");
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/src/__tests__/functional-component-effect.test.tsx:
--------------------------------------------------------------------------------
1 | import Taro, {useEffect, useState} from '@tarojs/taro';
2 | import {Text, View} from "@tarojs/components";
3 | import { render } from '../index';
4 | import {StandardProps} from "@tarojs/components/types/common";
5 |
6 |
7 | interface CounterProps extends StandardProps {
8 | initial?: number;
9 | }
10 |
11 | const Counter = (props: CounterProps) => {
12 | const {initial} = props
13 | const [count, setCount] = useState(0)
14 | useEffect(() => {
15 | setCount(initial || 1)
16 | }, [initial])
17 | return (
18 | {setCount(count+1)}}
20 | className="number custom-class"
21 | >{count}
22 | );
23 | };
24 |
25 | const NestTest = () => {
26 | const [count, setCount] = useState(10)
27 | return (
28 |
29 | {count}
30 | {setCount(20)}}>CLICK
31 |
32 | );
33 | };
34 |
35 |
36 | describe('useEffect test', () => {
37 | it('should excute useEffect method', () => {
38 | const { container } = render();
39 | const number = container.querySelector('.number') as HTMLSpanElement;
40 | expect(number.innerHTML).toEqual('1');
41 | });
42 |
43 | it('should excute useEffect method', () => {
44 | const { container } = render();
45 | const number = container.querySelector('.number') as HTMLSpanElement;
46 | expect(number.innerHTML).toEqual('10');
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/src/__tests__/functional-component.test.tsx:
--------------------------------------------------------------------------------
1 | import Taro, {useEffect, useState} from '@tarojs/taro';
2 | import {Text} from "@tarojs/components";
3 | import {StandardProps} from "@tarojs/components/types/common";
4 | import { act, render, renderToString } from '../index';
5 |
6 | interface CounterProps extends StandardProps {
7 | initial?: number;
8 | }
9 |
10 | const Counter = (props: CounterProps) => {
11 | const [count, setCount] = useState(props.initial || 1)
12 | return (
13 | {setCount(count+1)}}
15 | className="number custom-class"
16 | >{count}
17 | );
18 | };
19 |
20 | describe('functional component test', () => {
21 | it('should render component', () => {
22 | const { container } = render();
23 | const $number = container.querySelector('.number') as HTMLSpanElement;
24 | expect($number.innerHTML).toEqual('1');
25 | });
26 |
27 | it('should render component with props', () => {
28 | const initial = 10
29 | const { container } = render();
30 | const $number = container.querySelector('.number') as HTMLSpanElement;
31 | expect($number.innerHTML).toEqual(`${initial}`);
32 | });
33 |
34 | it('should render component with props', () => {
35 | const initial = 10
36 | const { container } = render();
37 | const $number = container.querySelector('.number') as HTMLSpanElement;
38 | expect($number.innerHTML).toEqual(`${initial}`);
39 | });
40 |
41 | it('should rerender when trigger setState hooks', () => {
42 | const { container } = render();
43 | const $number = container.querySelector('.number') as HTMLSpanElement;
44 | act(() => {
45 | $number.click()
46 | })
47 | expect($number.innerHTML).toEqual(`2`);
48 | });
49 |
50 | it('should rerender when excute rerender methods', () => {
51 | const { container, rerender } = render();
52 | expect(container.querySelector('.number').innerHTML).toEqual("1");
53 | rerender()
54 | expect(container.querySelector('.number').innerHTML).toEqual("2");
55 | });
56 |
57 | it('should support snapshot', () => {
58 | const component = renderToString(component without state
);
59 | expect(component).toMatchSnapshot();
60 | })
61 | });
62 |
--------------------------------------------------------------------------------
/src/__tests__/snapshot-component.test.tsx:
--------------------------------------------------------------------------------
1 | import Taro, {Component} from '@tarojs/taro';
2 | import { render } from '../index';
3 | import {StandardProps} from "@tarojs/components/types/common";
4 | import {Text} from "@tarojs/components";
5 |
6 | interface CounterProps extends StandardProps {
7 | initial?: number;
8 | }
9 |
10 | interface CounterState {
11 | count: number;
12 | }
13 |
14 | class Counter extends Component {
15 |
16 | static defaultProps = {
17 | initial: 1
18 | }
19 |
20 | static externalClasses = ['custom-class'];
21 |
22 | constructor(props: CounterProps) {
23 | super(props);
24 | this.state = {
25 | count: props.initial as number
26 | }
27 | }
28 |
29 | render() {
30 | const { count } = this.state
31 | return (
32 | {this.setState({
34 | count: count + 1
35 | })}}
36 | className="number custom-class"
37 | >{count}
38 | )
39 | }
40 | }
41 |
42 | describe('snapshot test', () => {
43 | it('should pass snapshot', () => {
44 | const { baseElement } = render();
45 | expect(baseElement).toMatchSnapshot()
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { getQueriesForElement, prettyDOM } from '@testing-library/dom';
2 | import Nerv, { unmountComponentAtNode, render as taroRender } from 'nervjs';
3 | export { renderToString } from 'nerv-server';
4 |
5 | global.Nerv = Nerv;
6 |
7 | export * from '@testing-library/dom';
8 |
9 | interface RenderOptions {
10 | container?: HTMLElement;
11 | target?: HTMLElement;
12 | [key: string]: any;
13 | }
14 |
15 | type Container = {
16 | target: HTMLElement;
17 | component: any;
18 | };
19 |
20 | const mountedContainers: Map = new Map();
21 |
22 | const cleanupAtContainer = (container: Container) => {
23 | const { target } = container;
24 | unmountComponentAtNode(target);
25 | if (target.parentNode) {
26 | target.parentNode.removeChild(target);
27 | }
28 | mountedContainers.delete(target);
29 | };
30 |
31 | export const render = (
32 | Component,
33 | {
34 | container = document.body,
35 | target = container.appendChild(document.createElement('div')),
36 | }: RenderOptions = {},
37 | ) => {
38 | let component
39 | const mount = (Component) => {
40 | component = taroRender(Component, target);
41 | mountedContainers.set(target, { target, component });
42 | component.forceUpdate();
43 | }
44 | mount(Component)
45 | return {
46 | get component() {
47 | return component;
48 | },
49 | get baseElement() {
50 | return target
51 | },
52 | debug: (el = container) => {
53 | console.log(prettyDOM(el));
54 | },
55 | container,
56 | unmount: () => cleanupAtContainer({ target, component }),
57 | ...getQueriesForElement(target),
58 | rerender: (Component) => {
59 | unmountComponentAtNode(target);
60 | mount(Component);
61 | }
62 | };
63 | };
64 |
65 | export const cleanup = () => {
66 | mountedContainers.forEach(cleanupAtContainer);
67 | };
68 |
69 | export default render;
70 |
71 | export * from '@testing-library/dom';
72 |
73 | export function flush(fn?: () => void) {
74 | return new Promise(function(resolve) {
75 | fn && fn();
76 | setTimeout(() => {
77 | resolve();
78 | }, 10); // # TODO: tricky for Nerv.js useEffect
79 | });
80 | }
81 |
82 | export function act(fn?: () => void) {
83 | fn && fn();
84 | mountedContainers.forEach(({component}) => {
85 | component.forceUpdate();
86 | })
87 | }
88 |
89 | // if we're running in a test runner that supports afterEach
90 | // then we'll automatically run cleanup afterEach test
91 | // this ensures that tests run in isolation from each other
92 | // if you don't like this then either import the `pure` module
93 | // or set the TTL_SKIP_AUTO_CLEANUP env variable to 'true'.
94 | // inspired from react-testing-library
95 | if (typeof afterEach === 'function' && !process.env.TTL_SKIP_AUTO_CLEANUP) {
96 | afterEach(() => {
97 | cleanup();
98 | });
99 | }
100 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "jest": true
4 | },
5 | "globals": {
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/test/unit/setup.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksChina/taro-testing-library/b981062b78645a189408b4b04d190e3659e1254b/test/unit/setup.js
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "ES2015",
5 | "moduleResolution": "node",
6 | "removeComments": false,
7 | "preserveConstEnums": true,
8 | "experimentalDecorators": true,
9 | "noImplicitAny": false,
10 | "allowSyntheticDefaultImports": true,
11 | "outDir": "lib",
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "strictNullChecks": true,
15 | "sourceMap": true,
16 | "baseUrl": ".",
17 | "rootDir": ".",
18 | "jsx": "preserve",
19 | "jsxFactory": "Taro.createElement",
20 | "allowJs": true,
21 | "resolveJsonModule": true
22 | },
23 | "exclude": [
24 | "node_modules",
25 | "lib",
26 | "**/test/**/*",
27 | "**/__stories__/**/*",
28 | "*.test.ts",
29 | "*.test.tsx"
30 | ],
31 | "compileOnSave": false
32 | }
33 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS{
2 | interface Global {
3 | Nerv: import('nervjs');
4 | }
5 | }
--------------------------------------------------------------------------------