230 lines
5.8 KiB
TypeScript
230 lines
5.8 KiB
TypeScript
/* eslint @typescript-eslint/no-explicit-any: 0 */
|
|
import { createStore, SelectState, WaitMiddleware } from '@rondo.dev/redux'
|
|
import React from 'react'
|
|
import ReactDOM from 'react-dom'
|
|
import T from 'react-dom/test-utils'
|
|
import { Provider } from 'react-redux'
|
|
import { Action, AnyAction, combineReducers, Reducer, ReducersMapObject } from 'redux'
|
|
import { ThemeProvider, DefaultTheme } from 'styled-components'
|
|
|
|
interface RenderParams<State, LocalState> {
|
|
reducers: ReducersMapObject<State, any>
|
|
select: SelectState<State, LocalState>
|
|
}
|
|
|
|
export class TestContainer extends React.Component<{}> {
|
|
ref = React.createRef<HTMLDivElement>()
|
|
render() {
|
|
return (
|
|
<div className='test-container' ref={this.ref}>
|
|
{this.props.children}
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
export class Selector {
|
|
constructor(
|
|
readonly element: Element = document.body,
|
|
) {
|
|
expect(this.element).toBeDefined()
|
|
}
|
|
|
|
static findManyByQuery(root: Element, query: string): Selector[] {
|
|
return Array
|
|
.from(root.querySelectorAll(query))
|
|
.map(el => new Selector(el))
|
|
}
|
|
|
|
static findManyByQueryAndCondition(
|
|
root: Element,
|
|
query: string,
|
|
verify: (selector: Selector) => boolean,
|
|
): Selector[] {
|
|
return this.findManyByQuery(root, query)
|
|
.filter(verify)
|
|
}
|
|
|
|
static findOneByQuery(root: Element, query: string): Selector {
|
|
const result = this.findManyByQuery(root, query)
|
|
expect(result.length).toBe(1)
|
|
return result[0]
|
|
}
|
|
|
|
static findOneByQueryAndCondition(
|
|
root: Element,
|
|
query: string,
|
|
verify: (selector: Selector) => boolean,
|
|
): Selector {
|
|
const result = this.findManyByQueryAndCondition(root, query, verify)
|
|
expect(result.length).toBe(1)
|
|
return result[0]
|
|
}
|
|
|
|
type(value: string) {
|
|
T.Simulate.change(this.element, {
|
|
target: {
|
|
value,
|
|
} as any,
|
|
})
|
|
}
|
|
click() {
|
|
T.Simulate.click(this.element)
|
|
}
|
|
submit() {
|
|
T.Simulate.submit(this.element)
|
|
}
|
|
findOne(selector: string): Selector {
|
|
return Selector.findOneByQuery(this.element, selector)
|
|
}
|
|
findOneByContent(selector: string, content: string): Selector {
|
|
return Selector.findOneByQueryAndCondition(
|
|
this.element, selector, sel => sel.content() === content)
|
|
}
|
|
findOneByValue(selector: string, value: string): Selector {
|
|
return Selector.findOneByQueryAndCondition(
|
|
this.element, selector, sel => sel.value() === value)
|
|
}
|
|
value(): string {
|
|
return (this.element as HTMLInputElement).value
|
|
}
|
|
tag(): string {
|
|
return this.element.tagName
|
|
}
|
|
content(): string | null {
|
|
return this.element.textContent
|
|
}
|
|
}
|
|
|
|
export function select(element: Element): Selector {
|
|
return new Selector(element)
|
|
}
|
|
|
|
export class TestUtils {
|
|
static defaultTheme?: DefaultTheme
|
|
|
|
/**
|
|
* Create a redux store
|
|
*/
|
|
readonly createStore = createStore
|
|
readonly Utils = T
|
|
|
|
async render(jsx: JSX.Element) {
|
|
const $div = document.createElement('div')
|
|
const component: TestContainer | null = await new Promise(resolve => {
|
|
ReactDOM.render(
|
|
<TestContainer ref={instance => resolve(instance)}>
|
|
<ThemeProvider theme={TestUtils.defaultTheme}>
|
|
{jsx}
|
|
</ThemeProvider>
|
|
</TestContainer>,
|
|
$div,
|
|
)
|
|
})
|
|
if (component === null) {
|
|
throw new Error('TestContainer is null, this should not happen')
|
|
}
|
|
return {
|
|
component,
|
|
node: $div.children[0].children[0],
|
|
}
|
|
}
|
|
|
|
combineReducers<S>(reducers: ReducersMapObject<S, any>): Reducer<S>
|
|
combineReducers<S, A extends Action = AnyAction>(
|
|
reducers: ReducersMapObject<S, A>,
|
|
): Reducer<S, A> {
|
|
return combineReducers(reducers)
|
|
}
|
|
|
|
/**
|
|
* Creates a redux store, connects a component, and provides the `render`
|
|
* method to render the connected component with a `Provider`.
|
|
*/
|
|
withProvider<State, LocalState, A extends Action<any> = AnyAction>(
|
|
params: RenderParams<State, LocalState>,
|
|
) {
|
|
const {reducers, select} = params
|
|
|
|
const waitMiddleware = new WaitMiddleware()
|
|
|
|
let store = this.createStore({
|
|
reducer: this.combineReducers(reducers),
|
|
extraMiddleware: [waitMiddleware.handle],
|
|
})
|
|
|
|
const withState = (state: Partial<State>) => {
|
|
store = this.createStore({
|
|
reducer: this.combineReducers(reducers),
|
|
state,
|
|
})
|
|
|
|
return {withComponent}
|
|
}
|
|
|
|
const withComponent = <Props extends {}>(
|
|
getComponent: (select: SelectState<State, LocalState>) =>
|
|
React.ComponentType<Props>,
|
|
) => {
|
|
const Component = getComponent(select)
|
|
|
|
type CreateJSX = (
|
|
Component: React.ComponentType<Props>,
|
|
props: Props,
|
|
) => JSX.Element
|
|
|
|
let createJSX: CreateJSX | undefined
|
|
|
|
const render = async (props: Props) => {
|
|
const recorder = waitMiddleware.record()
|
|
|
|
const jsx = createJSX
|
|
? createJSX(Component, props)
|
|
: <Component {...props} />
|
|
|
|
const result = await this.render(
|
|
<Provider store={store}>
|
|
{jsx}
|
|
</Provider>,
|
|
)
|
|
return {
|
|
...result,
|
|
async waitForActions(timeout = 2000) {
|
|
await waitMiddleware.waitForRecorded(recorder, timeout)
|
|
},
|
|
}
|
|
}
|
|
|
|
const withJSX = (localCreateJSX: CreateJSX) => {
|
|
createJSX = localCreateJSX
|
|
return self
|
|
}
|
|
|
|
const self: Self<
|
|
Props, typeof store, typeof Component, CreateJSX
|
|
> = {
|
|
render,
|
|
store,
|
|
Component,
|
|
withJSX,
|
|
}
|
|
|
|
return self
|
|
}
|
|
|
|
return {withState, withComponent}
|
|
}
|
|
}
|
|
|
|
interface Self<Props, Store, Component, CreateJSX> {
|
|
render: (props: Props) => Promise<{
|
|
component: TestContainer
|
|
node: Element
|
|
waitForActions(timeout?: number): Promise<void>
|
|
}>
|
|
store: Store
|
|
Component: Component
|
|
withJSX: (localCreateJSX: CreateJSX)
|
|
=> Self<Props, Store, Component, CreateJSX>
|
|
}
|