-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
import testUntyped from 'ava'; | ||
import { act, create, ReactTestRenderer } from 'react-test-renderer'; | ||
import React from 'react'; | ||
import { empty, of, BehaviorSubject } from 'rxjs'; | ||
import { connect } from './connect'; | ||
import { spy, SinonSpy } from 'sinon'; | ||
import { TestInterface } from 'ava'; | ||
|
||
type Props = { msg: string; num?: number }; | ||
const initialProps = { msg: 'hello' }; | ||
const secondProps = { msg: 'second' }; | ||
const additionalProps = { num: 3 }; | ||
const secondAdditionalProps = { num: 7 }; | ||
const WrappedComponent = ({ msg }: Props) => React.createElement('p', {}, msg); | ||
|
||
type Context = { | ||
WrappedComponentSpy: SinonSpy<[Props], ReturnType<typeof WrappedComponent>>; | ||
}; | ||
|
||
const test = testUntyped as TestInterface<Context>; | ||
|
||
test.beforeEach((t) => { | ||
t.context.WrappedComponentSpy = spy(({ msg }: Props) => | ||
React.createElement('p', {}, msg) | ||
); | ||
}); | ||
|
||
test.afterEach((t) => t.context.WrappedComponentSpy.resetHistory()); | ||
|
||
test('connect should render null on first render', (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const HOComponent = connect(WrappedComponentSpy, empty()); | ||
|
||
let component: ReactTestRenderer; | ||
act(() => { | ||
component = create(React.createElement(HOComponent)); | ||
}); | ||
|
||
t.assert(WrappedComponentSpy.notCalled); | ||
t.deepEqual(component!.toJSON(), null); | ||
}); | ||
|
||
test('connect should immediately send props to wrapped component', (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const HOComponent = connect(WrappedComponentSpy, of(initialProps)); | ||
|
||
act(() => { | ||
create(React.createElement(HOComponent)); | ||
}); | ||
|
||
t.deepEqual(WrappedComponentSpy.firstCall.args[0], initialProps); | ||
}); | ||
|
||
test('connect should re-render wrapped component on emitted props', (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const props$ = new BehaviorSubject<Props>(initialProps); | ||
const HOComponent = connect(WrappedComponentSpy, props$); | ||
|
||
act(() => { | ||
create(React.createElement(HOComponent)); | ||
}); | ||
t.deepEqual(WrappedComponentSpy.firstCall.args[0], initialProps); | ||
|
||
act(() => { | ||
props$.next(secondProps); | ||
}); | ||
t.deepEqual(WrappedComponentSpy.secondCall.args[0], secondProps); | ||
}); | ||
|
||
test('connect should unsubscribe stream when unmounted', async (t) => { | ||
const props$ = new BehaviorSubject<Props>(initialProps); | ||
const HOComponent = connect(WrappedComponent, props$); | ||
|
||
let component: ReactTestRenderer; | ||
act(() => { | ||
component = create(React.createElement(HOComponent)); | ||
}); | ||
|
||
t.notDeepEqual(props$.observers, []); | ||
|
||
act(() => { | ||
component.unmount(); | ||
}); | ||
|
||
t.deepEqual(props$.observers, []); | ||
}); | ||
|
||
test('connect should forward props', async (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const HOComponent = connect(WrappedComponentSpy, of(initialProps)); | ||
|
||
act(() => { | ||
create(React.createElement(HOComponent, additionalProps)); | ||
}); | ||
|
||
t.deepEqual(WrappedComponentSpy.firstCall?.args[0], { | ||
...additionalProps, | ||
...initialProps, | ||
}); | ||
}); | ||
|
||
test('connect give streamed props precedence over forwarded props', (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const HOComponent = connect(WrappedComponentSpy, of(initialProps)); | ||
|
||
act(() => { | ||
create( | ||
React.createElement(HOComponent, { | ||
...additionalProps, | ||
msg: 'overriding should not work', | ||
}) | ||
); | ||
}); | ||
|
||
t.deepEqual(WrappedComponentSpy.firstCall?.args[0], { | ||
...additionalProps, | ||
...initialProps, | ||
}); | ||
}); | ||
|
||
test('connect should propagate changes to forwarded props', async (t) => { | ||
const { WrappedComponentSpy } = t.context; | ||
const HOComponent = connect(WrappedComponentSpy, of(initialProps)); | ||
|
||
let component: ReactTestRenderer; | ||
act(() => { | ||
component = create(React.createElement(HOComponent, additionalProps)); | ||
}); | ||
act(() => { | ||
component.update(React.createElement(HOComponent, secondAdditionalProps)); | ||
}); | ||
|
||
t.deepEqual(WrappedComponentSpy.firstCall?.args[0], { | ||
...additionalProps, | ||
...initialProps, | ||
}); | ||
t.deepEqual(WrappedComponentSpy.secondCall?.args[0], { | ||
...secondAdditionalProps, | ||
...initialProps, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { useStream, NOT_YET_EMITTED } from './useStream'; | ||
import { Observable } from 'rxjs'; | ||
import React, { ComponentType } from 'react'; | ||
|
||
/** | ||
* Higher order component for connecting a component to a stream | ||
* | ||
* The stream does not need to provide all the properties the component expects, | ||
* any missing properties will be required by the component returned from this | ||
* component. | ||
* | ||
* Typescript does, in certain cases, allow for extra properties to be unpacked | ||
* on a component. This means it may be possible to send props which would be | ||
* provided by the stream. In these cases the values from the stream will | ||
* override any values passed as props. | ||
* | ||
* @param WrappedComponent Component that will recieve props from the stream | ||
* @param stream$ Stream that will provide props to the component | ||
* @see useStream | ||
*/ | ||
export const connect = <Props, Observed extends Partial<Props>>( | ||
Component: ComponentType<Props>, | ||
stream$: Observable<Observed> | ||
) => (props: Omit<Props, keyof Observed>) => { | ||
const value = useStream(stream$); | ||
|
||
if (value === NOT_YET_EMITTED) return null; | ||
|
||
// Typescript doesn't recognize this as Observed & Props for some reason | ||
// Question on StackOverflow: https://stackoverflow.com/q/60758084/1104307 | ||
const newProps = { ...props, ...value } as Props & Observed; | ||
|
||
return React.createElement(Component, newProps); | ||
}; |