Make react testing asynchronous
This commit is contained in:
parent
96c2565887
commit
adb3d877a4
4
TODO.md
4
TODO.md
@ -31,7 +31,7 @@
|
||||
- [ ] Styled components
|
||||
- [x] SSR
|
||||
- [ ] Replace bulma/blommer css framework with styled components
|
||||
- [ ] Check if restyped still significantly slows down TS compilation
|
||||
- [ ] Check if reakit still significantly slows down TS compilation
|
||||
|
||||
- [x] Use JSON schema instead of @Entity decorators
|
||||
- [x] Extract database into a separate module
|
||||
@ -55,7 +55,7 @@
|
||||
- [ ] Open Source machine learning model for posts, if it exists
|
||||
- [x] svg-captcha from npm. Could prevent blind users from creating an account. Also, new ML models can probably read this format.
|
||||
- [x] Possible solution: use TTS like say.js to generate audio. Use tasq lib to prevent too much cpu usage if more users arrive.
|
||||
- [ ] FIXME debug why espeak sometimes does not stream responses:w
|
||||
- [x] FIXME debug why espeak sometimes does not stream responses:w
|
||||
- [ ] Add privacy policy statement
|
||||
|
||||
- [ ] Email
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import React from 'react'
|
||||
import {MemoryRouter} from 'react-router'
|
||||
import {Route} from 'react-router-dom'
|
||||
import {Link} from './Link'
|
||||
import {TestUtils} from '../test-utils'
|
||||
import { MemoryRouter } from 'react-router'
|
||||
import { Route } from 'react-router-dom'
|
||||
import { TestUtils } from '../test-utils'
|
||||
import { Link } from './Link'
|
||||
|
||||
describe('Link', () => {
|
||||
|
||||
const t = new TestUtils()
|
||||
|
||||
function render(
|
||||
async function render(
|
||||
to: string,
|
||||
routePath = '/test',
|
||||
routerEntry = '/test',
|
||||
) {
|
||||
return t.render(
|
||||
return await t.render(
|
||||
<MemoryRouter initialEntries={[routerEntry]}>
|
||||
<Route
|
||||
path={routePath}
|
||||
@ -26,16 +26,16 @@ describe('Link', () => {
|
||||
)
|
||||
}
|
||||
|
||||
it('should set href to value', () => {
|
||||
const {node} = render('/my/link')
|
||||
it('should set href to value', async () => {
|
||||
const {node} = await render('/my/link')
|
||||
const a = node.querySelector('a') as HTMLElement
|
||||
expect(a.tagName).toEqual('A')
|
||||
expect(a.getAttribute('href')).toEqual('/my/link')
|
||||
expect(a.textContent).toEqual('Link Text')
|
||||
})
|
||||
|
||||
it('should format url to matched props', () => {
|
||||
const {node} = render(
|
||||
it('should format url to matched props', async () => {
|
||||
const {node} = await render(
|
||||
'/my/:oneId/link/:twoId',
|
||||
'/one/:oneId/two/:twoId',
|
||||
'/one/1/two/2',
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
test.todo('Implement and test Modal')
|
||||
// import React from 'react'
|
||||
// import T from 'react-dom/test-utils'
|
||||
// import {Modal} from './Modal'
|
||||
|
||||
@ -8,8 +8,8 @@ const t = new TestUtils()
|
||||
describe('Crumb', () => {
|
||||
|
||||
describe('render', () => {
|
||||
it('renders', () => {
|
||||
const {node} = t.render(
|
||||
it('renders', async () => {
|
||||
const {node} = await t.render(
|
||||
<MemoryRouter>
|
||||
<Crumb
|
||||
links={[{
|
||||
|
||||
@ -27,8 +27,8 @@ describe('configrueCrumbs', () => {
|
||||
.withJSX(Component => <MemoryRouter><Component /></MemoryRouter>)
|
||||
|
||||
describe('render', () => {
|
||||
it('renders', () => {
|
||||
const {node} = createTestCase().render({})
|
||||
it('renders', async () => {
|
||||
const {node} = await createTestCase().render({})
|
||||
|
||||
expect(node.innerHTML).toMatch(/href="\/one"/)
|
||||
expect(node.innerHTML).toMatch(/href="\/two"/)
|
||||
@ -37,14 +37,14 @@ describe('configrueCrumbs', () => {
|
||||
})
|
||||
|
||||
describe('BREADCRUMBS_SET', () => {
|
||||
it('updates breadcrumbs', () => {
|
||||
it('updates breadcrumbs', async () => {
|
||||
const {render, store} = createTestCase()
|
||||
const actions = new Feature.CrumbsActions()
|
||||
store.dispatch(actions.setCrumbs({
|
||||
links: [],
|
||||
current: 'Crumbtest',
|
||||
}))
|
||||
const {node} = render({})
|
||||
const {node} = await render({})
|
||||
expect(node.innerHTML).toMatch(/Crumbtest/)
|
||||
})
|
||||
})
|
||||
|
||||
@ -40,7 +40,7 @@ describe('configureLogin', () => {
|
||||
const onSuccess = jest.fn()
|
||||
let node: Element
|
||||
let component: TestContainer
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
http.mockAdd({
|
||||
method: 'post',
|
||||
url: '/auth/login',
|
||||
@ -48,7 +48,7 @@ describe('configureLogin', () => {
|
||||
}, {id: 123})
|
||||
|
||||
const t = createTestProvider()
|
||||
const r = t.render({onSuccess})
|
||||
const r = await t.render({onSuccess})
|
||||
node = r.node
|
||||
component = r.component
|
||||
T.Simulate.change(
|
||||
|
||||
@ -46,14 +46,14 @@ describe('configureRegister', () => {
|
||||
const onSuccess = jest.fn()
|
||||
let node: Element
|
||||
let component: TestContainer
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
http.mockAdd({
|
||||
method: 'post',
|
||||
url: '/auth/register',
|
||||
data,
|
||||
}, {id: 123})
|
||||
|
||||
const r = createTestProvider().render({onSuccess})
|
||||
const r = await createTestProvider().render({onSuccess})
|
||||
node = r.node
|
||||
component = r.component
|
||||
T.Simulate.change(
|
||||
|
||||
@ -69,7 +69,7 @@ describe('TeamConnector', () => {
|
||||
}
|
||||
|
||||
it('it fetches user teams on render', async () => {
|
||||
const {node, waitForActions} = createTestProvider().render({
|
||||
const {node, waitForActions} = await createTestProvider().render({
|
||||
history,
|
||||
location: {} as any,
|
||||
match: {} as any,
|
||||
@ -94,7 +94,7 @@ describe('TeamConnector', () => {
|
||||
}
|
||||
teamClientMock.create.mockResolvedValue(newTeam)
|
||||
const {render, store} = createTestProvider()
|
||||
const {waitForActions, node} = render({
|
||||
const {waitForActions, node} = await render({
|
||||
history,
|
||||
location: {} as any,
|
||||
match: {} as any,
|
||||
@ -113,7 +113,7 @@ describe('TeamConnector', () => {
|
||||
it('displays an error', async () => {
|
||||
teamClientMock.create.mockRejectedValue(new Error('Test Error'))
|
||||
const {render} = createTestProvider()
|
||||
const {node, waitForActions} = render({
|
||||
const {node, waitForActions} = await render({
|
||||
history,
|
||||
location: {} as any,
|
||||
match: {} as any,
|
||||
|
||||
@ -13,8 +13,8 @@ describe('Captcha', () => {
|
||||
|
||||
let node: Element
|
||||
let component: Captcha
|
||||
function render() {
|
||||
const result = t.render(
|
||||
async function render() {
|
||||
const result = await t.render(
|
||||
<Captcha
|
||||
audioUrl='/captcha.opus'
|
||||
imageUrl='/captcha.svg'
|
||||
|
||||
40
packages/react-test/src/index.test.tsx
Normal file
40
packages/react-test/src/index.test.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { TestUtils } from './index'
|
||||
import React from 'react'
|
||||
|
||||
describe('TestUtils', () => {
|
||||
|
||||
TestUtils.defaultTheme = {}
|
||||
const tu = new TestUtils()
|
||||
|
||||
const Functional: React.FunctionComponent<{}> = () => <span>fn</span>
|
||||
class Class extends React.PureComponent<{}> {
|
||||
render() {
|
||||
return <span>class</span>
|
||||
}
|
||||
}
|
||||
|
||||
describe('render', () => {
|
||||
it('renders a class component', async () => {
|
||||
const { node, component } = await tu.render(<Class />)
|
||||
expect(node).toBeTruthy()
|
||||
expect(node.tagName).toBe('SPAN')
|
||||
expect(node.innerHTML).toBe('class')
|
||||
tu.Utils.findRenderedComponentWithType(component, Class)
|
||||
})
|
||||
it('renders a functional component', async () => {
|
||||
const { node, component } = await tu.render(<Functional />)
|
||||
expect(node).toBeTruthy()
|
||||
expect(node.tagName).toBe('SPAN')
|
||||
expect(node.innerHTML).toBe('fn')
|
||||
expect(component).toBeTruthy()
|
||||
})
|
||||
it('renders a html component', async () => {
|
||||
const { node, component } = await tu.render(<span>text</span>)
|
||||
expect(node).toBeTruthy()
|
||||
expect(node.tagName).toBe('SPAN')
|
||||
expect(node.innerHTML).toBe('text')
|
||||
expect(component).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
@ -15,7 +15,11 @@ interface RenderParams<State, LocalState> {
|
||||
export class TestContainer extends React.Component<{}> {
|
||||
ref = React.createRef<HTMLDivElement>()
|
||||
render() {
|
||||
return <div ref={this.ref}>{this.props.children}</div>
|
||||
return (
|
||||
<div className='test-container' ref={this.ref}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,20 +32,24 @@ export class TestUtils {
|
||||
readonly createStore = createStore
|
||||
readonly Utils = T
|
||||
|
||||
render(jsx: JSX.Element) {
|
||||
async render(jsx: JSX.Element) {
|
||||
const $div = document.createElement('div')
|
||||
const component = ReactDOM.render(
|
||||
<TestContainer>
|
||||
<ThemeProvider theme={TestUtils.defaultTheme}>
|
||||
{jsx}
|
||||
</ThemeProvider>
|
||||
</TestContainer>,
|
||||
$div,
|
||||
) as unknown as TestContainer
|
||||
const node = component.ref.current!.children[0]
|
||||
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,
|
||||
node: $div.children[0].children[0],
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,14 +98,14 @@ export class TestUtils {
|
||||
|
||||
let createJSX: CreateJSX | undefined
|
||||
|
||||
const render = (props: Props) => {
|
||||
const render = async (props: Props) => {
|
||||
const recorder = waitMiddleware.record()
|
||||
|
||||
const jsx = createJSX
|
||||
? createJSX(Component, props)
|
||||
: <Component {...props} />
|
||||
|
||||
const result = this.render(
|
||||
const result = await this.render(
|
||||
<Provider store={store}>
|
||||
{jsx}
|
||||
</Provider>,
|
||||
@ -132,11 +140,11 @@ export class TestUtils {
|
||||
}
|
||||
|
||||
interface Self<Props, Store, Component, CreateJSX> {
|
||||
render: (props: Props) => {
|
||||
render: (props: Props) => Promise<{
|
||||
component: TestContainer
|
||||
node: Element
|
||||
waitForActions(timeout?: number): Promise<void>
|
||||
}
|
||||
}>
|
||||
store: Store
|
||||
Component: Component
|
||||
withJSX: (localCreateJSX: CreateJSX)
|
||||
|
||||
@ -17,7 +17,7 @@ describe('WaitMiddleware', () => {
|
||||
it('waits for certain async actions to be resolved', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
const promise = wm.wait({B: 1, C: 1})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
@ -40,7 +40,7 @@ describe('WaitMiddleware', () => {
|
||||
it('times out when actions do not happen', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'], 5)
|
||||
const promise = wm.wait({B: 1, C: 1}, 5)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
@ -58,7 +58,7 @@ describe('WaitMiddleware', () => {
|
||||
it('errors out when a promise is rejected', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B', 'C'])
|
||||
const promise = wm.wait({B: 1, C: 1})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'A',
|
||||
@ -76,8 +76,8 @@ describe('WaitMiddleware', () => {
|
||||
it('errors out when wait is called twice', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'])
|
||||
const error = await getError(wm.wait(['B']))
|
||||
const promise = wm.wait({B: 1})
|
||||
const error = await getError(wm.wait({B: 1}))
|
||||
expect(error.message).toMatch(/already waiting/)
|
||||
store.dispatch({
|
||||
payload: new Error('test'),
|
||||
@ -90,7 +90,7 @@ describe('WaitMiddleware', () => {
|
||||
it('does nothing when pending is dispatched', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const promise = wm.wait(['B'], 1)
|
||||
const promise = wm.wait({B: 1}, 1)
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
@ -102,7 +102,7 @@ describe('WaitMiddleware', () => {
|
||||
|
||||
it('resolved immediately when no actions are defined', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
await wm.wait([])
|
||||
await wm.wait({})
|
||||
})
|
||||
})
|
||||
|
||||
@ -129,11 +129,54 @@ describe('WaitMiddleware', () => {
|
||||
it('records custom actions', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const recorder = wm.record(action => action.type.startsWith('test'))
|
||||
const recorder = wm.record(
|
||||
action => action.type.startsWith('test'),
|
||||
action => action.type.startsWith('test.resolved'),
|
||||
action => action.type.startsWith('test.rejected'),
|
||||
)
|
||||
store.dispatch({type: 'test1'} as any)
|
||||
store.dispatch({type: 'tes'} as any)
|
||||
store.dispatch({type: 'test3'} as any)
|
||||
expect(recorder.getActionTypes()).toEqual(['test1', 'test3'])
|
||||
expect(recorder.getActionTypes()).toEqual({
|
||||
test1: 1,
|
||||
test3: 1,
|
||||
})
|
||||
})
|
||||
|
||||
it('does not wait for actions that have already resolved', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const recorder = wm.record()
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'pending',
|
||||
})
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'resolved',
|
||||
})
|
||||
await wm.waitForRecorded(recorder)
|
||||
})
|
||||
|
||||
it('errors out if an action is rejected', async () => {
|
||||
const wm = new WaitMiddleware()
|
||||
const store = getStore(wm)
|
||||
const recorder = wm.record()
|
||||
store.dispatch({
|
||||
payload: undefined,
|
||||
type: 'B',
|
||||
status: 'pending',
|
||||
})
|
||||
const error = new Error('test')
|
||||
store.dispatch({
|
||||
payload: error,
|
||||
type: 'A',
|
||||
status: 'rejected',
|
||||
})
|
||||
const error2 = await getError(wm.waitForRecorded(recorder))
|
||||
expect(error2).toBe(error)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -16,8 +16,12 @@ export class WaitMiddleware {
|
||||
/**
|
||||
* Starts recording actions and returns the recorder.
|
||||
*/
|
||||
record(shouldRecord: ShouldRecord = defaultShouldRecord): Recorder {
|
||||
const recorder = new Recorder(shouldRecord)
|
||||
record(
|
||||
shouldRecord: ShouldRecord = defaultShouldRecord,
|
||||
shouldResolve: ShouldRecord = defaultShouldResolve,
|
||||
shouldReject: ShouldRecord = defaultShouldReject,
|
||||
): Recorder {
|
||||
const recorder = new Recorder(shouldRecord, shouldResolve, shouldReject)
|
||||
this.recorders.push(recorder)
|
||||
return recorder
|
||||
}
|
||||
@ -36,6 +40,10 @@ export class WaitMiddleware {
|
||||
*/
|
||||
async waitForRecorded(recorder: Recorder, timeout?: number): Promise<void> {
|
||||
this.stopRecording(recorder)
|
||||
const error = recorder.getError()
|
||||
if (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
await this.wait(recorder.getActionTypes(), timeout)
|
||||
}
|
||||
|
||||
@ -43,19 +51,17 @@ export class WaitMiddleware {
|
||||
* Waits for actions to be resolved or rejected. Times out after 10 seconds
|
||||
* by default.
|
||||
*/
|
||||
async wait(actions: string[], timeout = 10000): Promise<void> {
|
||||
async wait(actions: Record<string, number>, timeout = 10000): Promise<void> {
|
||||
if (this.notify) {
|
||||
throw new Error('WaitMiddleware.wait - already waiting!')
|
||||
}
|
||||
|
||||
const actionsByName = actions.reduce((obj, type) => {
|
||||
obj[type] = (obj[type] || 0) + 1
|
||||
return obj
|
||||
}, {} as Record<string, number>)
|
||||
let count = actions.length
|
||||
let count = Object.keys(actions).reduce((count, type) => {
|
||||
return count + actions[type]
|
||||
}, 0)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!actions.length) {
|
||||
if (!count) {
|
||||
resolve()
|
||||
this.notify = undefined
|
||||
return
|
||||
@ -67,14 +73,14 @@ export class WaitMiddleware {
|
||||
}, timeout)
|
||||
|
||||
this.notify = (action: AsyncAction<unknown, string>) => {
|
||||
if (!actionsByName[action.type]) {
|
||||
if (!actions[action.type]) {
|
||||
return
|
||||
}
|
||||
switch (action.status) {
|
||||
case 'pending':
|
||||
return
|
||||
case 'resolved':
|
||||
actionsByName[action.type]--
|
||||
actions[action.type]--
|
||||
count--
|
||||
if (count === 0) {
|
||||
clearTimeout(t)
|
||||
@ -100,19 +106,39 @@ export type ShouldRecord = (action: AnyAction) => boolean
|
||||
|
||||
export const defaultShouldRecord: ShouldRecord =
|
||||
(action: AnyAction) => 'status' in action && action.status === 'pending'
|
||||
export const defaultShouldResolve: ShouldRecord =
|
||||
(action: AnyAction) => 'status' in action && action.status === 'resolved'
|
||||
export const defaultShouldReject: ShouldRecord =
|
||||
(action: AnyAction) => 'status' in action && action.status === 'rejected'
|
||||
|
||||
class Recorder {
|
||||
protected actionTypes: string[] = []
|
||||
protected actionTypes: Record<string, number> = {}
|
||||
protected error?: Error
|
||||
|
||||
constructor(protected readonly shouldRecord: ShouldRecord) {}
|
||||
constructor(
|
||||
protected readonly shouldRecord: ShouldRecord,
|
||||
protected readonly shouldResolve: ShouldRecord,
|
||||
protected readonly shouldReject: ShouldRecord,
|
||||
) {}
|
||||
|
||||
getActionTypes(): string[] {
|
||||
return this.actionTypes.slice()
|
||||
getActionTypes() {
|
||||
return {...this.actionTypes}
|
||||
}
|
||||
|
||||
getError() {
|
||||
return this.error
|
||||
}
|
||||
|
||||
record(action: AnyAction) {
|
||||
if (this.shouldRecord(action)) {
|
||||
this.actionTypes.push(action.type)
|
||||
this.actionTypes[action.type] = (this.actionTypes[action.type] || 0) + 1
|
||||
}
|
||||
if (this.shouldResolve(action)) {
|
||||
this.actionTypes[action.type] = (this.actionTypes[action.type] || 0) - 1
|
||||
}
|
||||
if (this.shouldReject(action)) {
|
||||
this.actionTypes[action.type] = (this.actionTypes[action.type] || 0) - 1
|
||||
this.error = action.payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user