Skip to content

Commit b7dbc7d

Browse files
committed
feat: init test and ci setup
1 parent d66acf2 commit b7dbc7d

File tree

11 files changed

+2282
-50
lines changed

11 files changed

+2282
-50
lines changed

.circleci/config.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
defaults: &defaults
2+
working_directory: ~/rxjs-hooks
3+
docker:
4+
- image: circleci/node:10-browsers
5+
6+
version: 2
7+
jobs:
8+
build:
9+
<<: *defaults
10+
steps:
11+
- checkout
12+
- run: echo 'export PATH=${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin' >> $BASH_ENV
13+
- restore_cache:
14+
key: dependency-cache-{{ checksum "package.json" }}
15+
- run:
16+
name: yarn-with-greenkeeper
17+
command: |
18+
sudo yarn global add greenkeeper-lockfile@1
19+
yarn
20+
- save_cache:
21+
key: dependency-cache-{{ checksum "package.json" }}
22+
paths:
23+
- ~/.cache/yarn
24+
- run: greenkeeper-lockfile-update
25+
- run: yarn build
26+
- run: greenkeeper-lockfile-upload
27+
- persist_to_workspace:
28+
root: ~/rxjs-hooks
29+
paths:
30+
- ./*
31+
test:
32+
<<: *defaults
33+
steps:
34+
- attach_workspace:
35+
at: ~/rxjs-hooks
36+
- run: yarn lint
37+
- run: yarn test
38+
- run:
39+
name: test-coverage
40+
command: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
41+
42+
deploy:
43+
<<: *defaults
44+
steps:
45+
- attach_workspace:
46+
at: ~/rxjs-hooks
47+
- run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
48+
- run: |
49+
if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$";
50+
then
51+
npm publish
52+
else
53+
echo "Not a release, skipping publish"
54+
fi
55+
workflows:
56+
version: 2
57+
build_test_and_deploy:
58+
jobs:
59+
- build
60+
- test:
61+
requires:
62+
- build
63+
- deploy:
64+
requires:
65+
- test
66+
filters:
67+
tags:
68+
only: /.*/
69+
branches:
70+
only: master

jest.config.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module.exports = {
2+
browser: true,
3+
verbose: true,
4+
rootDir: __dirname,
5+
setupTestFrameworkScriptFile: '<rootDir>/tools/test-setup.js',
6+
moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/src'],
7+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
8+
transform: {
9+
'^.+\\.tsx?$': 'ts-jest'
10+
},
11+
// testRegex: '/__test__/.*\\.spec\\.tsx?$',
12+
collectCoverage: true,
13+
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/__test__/**'],
14+
coverageReporters: ['lcov', 'html'],
15+
preset: 'ts-jest'
16+
}

package.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,36 @@
88
"scripts": {
99
"build": "rm -rf esm && tsc -p src/tsconfig.json --outDir esm",
1010
"lint": "tslint -c tslint.json -p src/tsconfig.json",
11-
"start": "webpack-dev-server --progress --color"
11+
"start": "webpack-dev-server --config ./tools/webpack.config.js --progress --color",
12+
"test": "NODE_ENV=test jest --no-cache --ci"
1213
},
1314
"repository": {
1415
"type": "git",
1516
"url": "git@github.com:LeetCode-OpenSource/rxjs-hooks.git"
1617
},
1718
"devDependencies": {
19+
"@types/jest": "^23.3.9",
1820
"@types/lodash": "^4.14.118",
1921
"@types/react-dom": "^16.0.9",
22+
"@types/react-test-renderer": "^16.0.3",
23+
"@types/sinon": "^5.0.5",
24+
"@types/sinon-chai": "^3.2.0",
2025
"@types/webpack": "^4.4.19",
2126
"fork-ts-checker-webpack-plugin": "^0.4.15",
2227
"happypack": "^5.0.0",
2328
"html-webpack-plugin": "^3.2.0",
2429
"husky": "^1.1.4",
30+
"jest": "^23.6.0",
2531
"lint-staged": "^8.0.4",
2632
"prettier": "^1.15.2",
2733
"react": "16.7.0-alpha.2",
2834
"react-dom": "16.7.0-alpha.2",
35+
"react-test-renderer": "^16.7.0-alpha.2",
2936
"rxjs": "^6.3.3",
37+
"sinon": "^7.1.1",
3038
"source-map-loader": "^0.2.4",
3139
"standard": "^12.0.1",
40+
"ts-jest": "^23.10.4",
3241
"ts-loader": "^5.3.0",
3342
"tslint": "^5.11.0",
3443
"tslint-config-prettier": "^1.16.0",

src/__test__/find.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ReactTestInstance } from 'react-test-renderer'
2+
3+
export function find(node: ReactTestInstance, type: string) {
4+
return node.find((node) => node.type === type)
5+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import React from 'react'
2+
import { Observable, of, Observer } from 'rxjs'
3+
import { mapTo, delay } from 'rxjs/operators'
4+
import { create } from 'react-test-renderer'
5+
import * as Sinon from 'sinon'
6+
7+
import { find } from './find'
8+
import { useEventCallback } from '../use-event-callback'
9+
10+
describe('useEventCallback specs', () => {
11+
function createFixture<T>(
12+
factory: (event$: Observable<React.SyntheticEvent<any>>) => Observable<T>,
13+
initialValue?: T,
14+
) {
15+
return function Fixture() {
16+
const [callback, value] = useEventCallback(factory, initialValue)
17+
return (
18+
<>
19+
<h1>{value}</h1>
20+
<button onClick={callback}>click me</button>
21+
</>
22+
)
23+
}
24+
}
25+
26+
it('should generate callback', () => {
27+
const Fixture = createFixture(() => of(1))
28+
const fixtureNode = <Fixture />
29+
const testRenderer = create(fixtureNode)
30+
testRenderer.update(fixtureNode)
31+
const button = find(testRenderer.root, 'button')
32+
expect(button.props.onClick.name).toBe('clickCallback')
33+
})
34+
35+
it('should render value', () => {
36+
const value = 1
37+
const Fixture = createFixture(() => of(value))
38+
const fixtureNode = <Fixture />
39+
const testRenderer = create(fixtureNode)
40+
expect(find(testRenderer.root, 'h1').children).toEqual([])
41+
testRenderer.update(fixtureNode)
42+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
43+
})
44+
45+
it('should trigger handler async callback', () => {
46+
const timer = Sinon.useFakeTimers()
47+
const timeToDelay = 200
48+
const value = 1
49+
const Fixture = createFixture((event$: Observable<any>) =>
50+
event$.pipe(
51+
mapTo(value),
52+
delay(timeToDelay),
53+
),
54+
)
55+
const fixtureNode = <Fixture />
56+
const testRenderer = create(fixtureNode)
57+
testRenderer.update(fixtureNode)
58+
const button = find(testRenderer.root, 'button')
59+
button.props.onClick()
60+
timer.tick(timeToDelay)
61+
testRenderer.update(fixtureNode)
62+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
63+
timer.restore()
64+
})
65+
66+
it('should handler the initial value', () => {
67+
const timer = Sinon.useFakeTimers()
68+
const initialValue = 1000
69+
const value = 1
70+
const timeToDelay = 200
71+
const Fixture = createFixture(
72+
(event$: Observable<any>) =>
73+
event$.pipe(
74+
mapTo(value),
75+
delay(timeToDelay),
76+
),
77+
initialValue,
78+
)
79+
const fixtureNode = <Fixture />
80+
const testRenderer = create(fixtureNode)
81+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
82+
testRenderer.update(fixtureNode)
83+
const button = find(testRenderer.root, 'button')
84+
button.props.onClick()
85+
timer.tick(timeToDelay)
86+
testRenderer.update(fixtureNode)
87+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
88+
timer.restore()
89+
})
90+
91+
it('should call teardown logic after unmount', () => {
92+
const spy = Sinon.spy()
93+
const Fixture = createFixture(
94+
() =>
95+
new Observable((observer: Observer<number>) => {
96+
const timerId = setTimeout(() => {
97+
observer.next(1)
98+
observer.complete()
99+
}, 1000)
100+
return () => {
101+
spy()
102+
clearTimeout(timerId)
103+
}
104+
}),
105+
)
106+
const fixtureNode = <Fixture />
107+
const testRenderer = create(fixtureNode)
108+
testRenderer.unmount()
109+
expect(spy.callCount).toBe(1)
110+
})
111+
})
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React from 'react'
2+
import { create } from 'react-test-renderer'
3+
import * as Sinon from 'sinon'
4+
import { of, Observable, Observer } from 'rxjs'
5+
6+
import { find } from './find'
7+
import { useObservable } from '../use-observable'
8+
import { tap } from 'rxjs/operators'
9+
10+
describe('useObservable specs', () => {
11+
let timer: Sinon.SinonFakeTimers
12+
13+
beforeEach(() => {
14+
timer = Sinon.useFakeTimers()
15+
})
16+
17+
afterEach(() => {
18+
timer.restore()
19+
})
20+
21+
it('should get value from sync Observable', () => {
22+
const value = 100
23+
function Fixture() {
24+
const value = useObservable(() => of(100))
25+
return <h1>{value}</h1>
26+
}
27+
const fixtureNode = <Fixture />
28+
29+
const testRenderer = create(fixtureNode)
30+
expect(find(testRenderer.root, 'h1').children).toEqual([])
31+
testRenderer.update(fixtureNode)
32+
33+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
34+
})
35+
36+
it('should render the initialValue', () => {
37+
const initialValue = 2000
38+
const value = 100
39+
function Fixture() {
40+
const value = useObservable(() => of(100), initialValue)
41+
return <h1>{value}</h1>
42+
}
43+
const fixtureNode = <Fixture />
44+
45+
const testRenderer = create(fixtureNode)
46+
expect(find(testRenderer.root, 'h1').children).toEqual([`${initialValue}`])
47+
testRenderer.update(fixtureNode)
48+
49+
expect(find(testRenderer.root, 'h1').children).toEqual([`${value}`])
50+
})
51+
52+
it('should call teardown logic after unmount', () => {
53+
const spy = Sinon.spy()
54+
function Fixture() {
55+
const value = useObservable(
56+
() =>
57+
new Observable((observer: Observer<number>) => {
58+
const timerId = setTimeout(() => {
59+
observer.next(1)
60+
observer.complete()
61+
}, 1000)
62+
return () => {
63+
spy()
64+
clearTimeout(timerId)
65+
}
66+
}),
67+
)
68+
return <h1>{value}</h1>
69+
}
70+
const fixtureNode = <Fixture />
71+
72+
const testRenderer = create(fixtureNode)
73+
expect(spy.callCount).toBe(0)
74+
testRenderer.unmount()
75+
expect(spy.callCount).toBe(1)
76+
})
77+
78+
it('should emit changed props in observableFactory', () => {
79+
const spy = Sinon.spy()
80+
function Fixture(props: { foo: number; bar: string; baz: any }) {
81+
const value = useObservable((props$: Observable<[number, any]>) => props$.pipe(tap(spy)), null, [
82+
props.foo,
83+
props.baz,
84+
] as any)
85+
return (
86+
<>
87+
<h1>{value && value[0]}</h1>
88+
<div>{value && value[1].foo}</div>
89+
</>
90+
)
91+
}
92+
93+
const props = {
94+
foo: 1,
95+
bar: 'bar',
96+
baz: {
97+
foo: 1,
98+
},
99+
}
100+
101+
const testRenderer = create(<Fixture {...props} />)
102+
expect(spy.callCount).toBe(0)
103+
expect(find(testRenderer.root, 'h1').children).toEqual([])
104+
expect(find(testRenderer.root, 'div').children).toEqual([])
105+
const newProps = { ...props, bar: 'new bar' }
106+
testRenderer.update(<Fixture {...newProps} />)
107+
expect(spy.callCount).toBe(1)
108+
expect(spy.args[0]).toEqual([[newProps.foo, newProps.baz]])
109+
expect(find(testRenderer.root, 'h1').children).toEqual([`${newProps.foo}`])
110+
expect(find(testRenderer.root, 'div').children).toEqual([`${newProps.baz.foo}`])
111+
112+
const renewProps = { ...props, foo: 1000 }
113+
testRenderer.update(<Fixture {...renewProps} />)
114+
115+
expect(spy.callCount).toBe(2)
116+
expect(spy.args[1]).toEqual([[renewProps.foo, renewProps.baz]])
117+
expect(find(testRenderer.root, 'h1').children).toEqual([`${renewProps.foo}`])
118+
expect(find(testRenderer.root, 'div').children).toEqual([`${renewProps.baz.foo}`])
119+
})
120+
})

src/use-event-callback.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export function useEventCallback<T, U = void>(
1717
useEffect(
1818
() => {
1919
const event$ = new Subject<SyntheticEvent<T>>()
20-
const clickCallback = (e: SyntheticEvent<T>) => event$.next(e)
20+
function clickCallback(e: SyntheticEvent<T>) {
21+
return event$.next(e)
22+
}
2123
setState([clickCallback, initialValue])
2224
const value$ = callback(event$)
2325
const subscription = value$.subscribe((value) => {

tools/test-setup.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
process.on('unhandledRejection', (reason) => {
2+
console.error(reason)
3+
})

webpack.config.js renamed to tools/webpack.config.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,6 @@ module.exports = {
4545
plugins: [
4646
new webpack.NamedModulesPlugin(),
4747

48-
new webpack.HotModuleReplacementPlugin(),
49-
5048
new HappyPack({
5149
id: 'sourcemap',
5250
loaders: ['source-map-loader'],

tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"suppressExcessPropertyErrors": true,
1616
"forceConsistentCasingInFileNames": true,
1717
"skipLibCheck": true,
18-
"declaration": true
18+
"declaration": true,
19+
"esModuleInterop": true
1920
}
2021
}

0 commit comments

Comments
 (0)