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
|
- [ ] Styled components
|
||||||
- [x] SSR
|
- [x] SSR
|
||||||
- [ ] Replace bulma/blommer css framework with styled components
|
- [ ] 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] Use JSON schema instead of @Entity decorators
|
||||||
- [x] Extract database into a separate module
|
- [x] Extract database into a separate module
|
||||||
@ -55,7 +55,7 @@
|
|||||||
- [ ] Open Source machine learning model for posts, if it exists
|
- [ ] 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] 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.
|
- [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
|
- [ ] Add privacy policy statement
|
||||||
|
|
||||||
- [ ] Email
|
- [ ] Email
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {MemoryRouter} from 'react-router'
|
import { MemoryRouter } from 'react-router'
|
||||||
import {Route} from 'react-router-dom'
|
import { Route } from 'react-router-dom'
|
||||||
import {Link} from './Link'
|
import { TestUtils } from '../test-utils'
|
||||||
import {TestUtils} from '../test-utils'
|
import { Link } from './Link'
|
||||||
|
|
||||||
describe('Link', () => {
|
describe('Link', () => {
|
||||||
|
|
||||||
const t = new TestUtils()
|
const t = new TestUtils()
|
||||||
|
|
||||||
function render(
|
async function render(
|
||||||
to: string,
|
to: string,
|
||||||
routePath = '/test',
|
routePath = '/test',
|
||||||
routerEntry = '/test',
|
routerEntry = '/test',
|
||||||
) {
|
) {
|
||||||
return t.render(
|
return await t.render(
|
||||||
<MemoryRouter initialEntries={[routerEntry]}>
|
<MemoryRouter initialEntries={[routerEntry]}>
|
||||||
<Route
|
<Route
|
||||||
path={routePath}
|
path={routePath}
|
||||||
@ -26,16 +26,16 @@ describe('Link', () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should set href to value', () => {
|
it('should set href to value', async () => {
|
||||||
const {node} = render('/my/link')
|
const {node} = await render('/my/link')
|
||||||
const a = node.querySelector('a') as HTMLElement
|
const a = node.querySelector('a') as HTMLElement
|
||||||
expect(a.tagName).toEqual('A')
|
expect(a.tagName).toEqual('A')
|
||||||
expect(a.getAttribute('href')).toEqual('/my/link')
|
expect(a.getAttribute('href')).toEqual('/my/link')
|
||||||
expect(a.textContent).toEqual('Link Text')
|
expect(a.textContent).toEqual('Link Text')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should format url to matched props', () => {
|
it('should format url to matched props', async () => {
|
||||||
const {node} = render(
|
const {node} = await render(
|
||||||
'/my/:oneId/link/:twoId',
|
'/my/:oneId/link/:twoId',
|
||||||
'/one/:oneId/two/:twoId',
|
'/one/:oneId/two/:twoId',
|
||||||
'/one/1/two/2',
|
'/one/1/two/2',
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
test.todo('Implement and test Modal')
|
||||||
// import React from 'react'
|
// import React from 'react'
|
||||||
// import T from 'react-dom/test-utils'
|
// import T from 'react-dom/test-utils'
|
||||||
// import {Modal} from './Modal'
|
// import {Modal} from './Modal'
|
||||||
|
|||||||
@ -8,8 +8,8 @@ const t = new TestUtils()
|
|||||||
describe('Crumb', () => {
|
describe('Crumb', () => {
|
||||||
|
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
it('renders', () => {
|
it('renders', async () => {
|
||||||
const {node} = t.render(
|
const {node} = await t.render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Crumb
|
<Crumb
|
||||||
links={[{
|
links={[{
|
||||||
|
|||||||
@ -27,8 +27,8 @@ describe('configrueCrumbs', () => {
|
|||||||
.withJSX(Component => <MemoryRouter><Component /></MemoryRouter>)
|
.withJSX(Component => <MemoryRouter><Component /></MemoryRouter>)
|
||||||
|
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
it('renders', () => {
|
it('renders', async () => {
|
||||||
const {node} = createTestCase().render({})
|
const {node} = await createTestCase().render({})
|
||||||
|
|
||||||
expect(node.innerHTML).toMatch(/href="\/one"/)
|
expect(node.innerHTML).toMatch(/href="\/one"/)
|
||||||
expect(node.innerHTML).toMatch(/href="\/two"/)
|
expect(node.innerHTML).toMatch(/href="\/two"/)
|
||||||
@ -37,14 +37,14 @@ describe('configrueCrumbs', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('BREADCRUMBS_SET', () => {
|
describe('BREADCRUMBS_SET', () => {
|
||||||
it('updates breadcrumbs', () => {
|
it('updates breadcrumbs', async () => {
|
||||||
const {render, store} = createTestCase()
|
const {render, store} = createTestCase()
|
||||||
const actions = new Feature.CrumbsActions()
|
const actions = new Feature.CrumbsActions()
|
||||||
store.dispatch(actions.setCrumbs({
|
store.dispatch(actions.setCrumbs({
|
||||||
links: [],
|
links: [],
|
||||||
current: 'Crumbtest',
|
current: 'Crumbtest',
|
||||||
}))
|
}))
|
||||||
const {node} = render({})
|
const {node} = await render({})
|
||||||
expect(node.innerHTML).toMatch(/Crumbtest/)
|
expect(node.innerHTML).toMatch(/Crumbtest/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -40,7 +40,7 @@ describe('configureLogin', () => {
|
|||||||
const onSuccess = jest.fn()
|
const onSuccess = jest.fn()
|
||||||
let node: Element
|
let node: Element
|
||||||
let component: TestContainer
|
let component: TestContainer
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
http.mockAdd({
|
http.mockAdd({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/auth/login',
|
url: '/auth/login',
|
||||||
@ -48,7 +48,7 @@ describe('configureLogin', () => {
|
|||||||
}, {id: 123})
|
}, {id: 123})
|
||||||
|
|
||||||
const t = createTestProvider()
|
const t = createTestProvider()
|
||||||
const r = t.render({onSuccess})
|
const r = await t.render({onSuccess})
|
||||||
node = r.node
|
node = r.node
|
||||||
component = r.component
|
component = r.component
|
||||||
T.Simulate.change(
|
T.Simulate.change(
|
||||||
|
|||||||
@ -46,14 +46,14 @@ describe('configureRegister', () => {
|
|||||||
const onSuccess = jest.fn()
|
const onSuccess = jest.fn()
|
||||||
let node: Element
|
let node: Element
|
||||||
let component: TestContainer
|
let component: TestContainer
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
http.mockAdd({
|
http.mockAdd({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
url: '/auth/register',
|
url: '/auth/register',
|
||||||
data,
|
data,
|
||||||
}, {id: 123})
|
}, {id: 123})
|
||||||
|
|
||||||
const r = createTestProvider().render({onSuccess})
|
const r = await createTestProvider().render({onSuccess})
|
||||||
node = r.node
|
node = r.node
|
||||||
component = r.component
|
component = r.component
|
||||||
T.Simulate.change(
|
T.Simulate.change(
|
||||||
|
|||||||
@ -69,7 +69,7 @@ describe('TeamConnector', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('it fetches user teams on render', async () => {
|
it('it fetches user teams on render', async () => {
|
||||||
const {node, waitForActions} = createTestProvider().render({
|
const {node, waitForActions} = await createTestProvider().render({
|
||||||
history,
|
history,
|
||||||
location: {} as any,
|
location: {} as any,
|
||||||
match: {} as any,
|
match: {} as any,
|
||||||
@ -94,7 +94,7 @@ describe('TeamConnector', () => {
|
|||||||
}
|
}
|
||||||
teamClientMock.create.mockResolvedValue(newTeam)
|
teamClientMock.create.mockResolvedValue(newTeam)
|
||||||
const {render, store} = createTestProvider()
|
const {render, store} = createTestProvider()
|
||||||
const {waitForActions, node} = render({
|
const {waitForActions, node} = await render({
|
||||||
history,
|
history,
|
||||||
location: {} as any,
|
location: {} as any,
|
||||||
match: {} as any,
|
match: {} as any,
|
||||||
@ -113,7 +113,7 @@ describe('TeamConnector', () => {
|
|||||||
it('displays an error', async () => {
|
it('displays an error', async () => {
|
||||||
teamClientMock.create.mockRejectedValue(new Error('Test Error'))
|
teamClientMock.create.mockRejectedValue(new Error('Test Error'))
|
||||||
const {render} = createTestProvider()
|
const {render} = createTestProvider()
|
||||||
const {node, waitForActions} = render({
|
const {node, waitForActions} = await render({
|
||||||
history,
|
history,
|
||||||
location: {} as any,
|
location: {} as any,
|
||||||
match: {} as any,
|
match: {} as any,
|
||||||
|
|||||||
@ -13,8 +13,8 @@ describe('Captcha', () => {
|
|||||||
|
|
||||||
let node: Element
|
let node: Element
|
||||||
let component: Captcha
|
let component: Captcha
|
||||||
function render() {
|
async function render() {
|
||||||
const result = t.render(
|
const result = await t.render(
|
||||||
<Captcha
|
<Captcha
|
||||||
audioUrl='/captcha.opus'
|
audioUrl='/captcha.opus'
|
||||||
imageUrl='/captcha.svg'
|
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<{}> {
|
export class TestContainer extends React.Component<{}> {
|
||||||
ref = React.createRef<HTMLDivElement>()
|
ref = React.createRef<HTMLDivElement>()
|
||||||
render() {
|
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 createStore = createStore
|
||||||
readonly Utils = T
|
readonly Utils = T
|
||||||
|
|
||||||
render(jsx: JSX.Element) {
|
async render(jsx: JSX.Element) {
|
||||||
const $div = document.createElement('div')
|
const $div = document.createElement('div')
|
||||||
const component = ReactDOM.render(
|
const component: TestContainer | null = await new Promise(resolve => {
|
||||||
<TestContainer>
|
ReactDOM.render(
|
||||||
<ThemeProvider theme={TestUtils.defaultTheme}>
|
<TestContainer ref={instance => resolve(instance)}>
|
||||||
{jsx}
|
<ThemeProvider theme={TestUtils.defaultTheme}>
|
||||||
</ThemeProvider>
|
{jsx}
|
||||||
</TestContainer>,
|
</ThemeProvider>
|
||||||
$div,
|
</TestContainer>,
|
||||||
) as unknown as TestContainer
|
$div,
|
||||||
const node = component.ref.current!.children[0]
|
)
|
||||||
|
})
|
||||||
|
if (component === null) {
|
||||||
|
throw new Error('TestContainer is null, this should not happen')
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
component,
|
component,
|
||||||
node,
|
node: $div.children[0].children[0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,14 +98,14 @@ export class TestUtils {
|
|||||||
|
|
||||||
let createJSX: CreateJSX | undefined
|
let createJSX: CreateJSX | undefined
|
||||||
|
|
||||||
const render = (props: Props) => {
|
const render = async (props: Props) => {
|
||||||
const recorder = waitMiddleware.record()
|
const recorder = waitMiddleware.record()
|
||||||
|
|
||||||
const jsx = createJSX
|
const jsx = createJSX
|
||||||
? createJSX(Component, props)
|
? createJSX(Component, props)
|
||||||
: <Component {...props} />
|
: <Component {...props} />
|
||||||
|
|
||||||
const result = this.render(
|
const result = await this.render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
{jsx}
|
{jsx}
|
||||||
</Provider>,
|
</Provider>,
|
||||||
@ -132,11 +140,11 @@ export class TestUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Self<Props, Store, Component, CreateJSX> {
|
interface Self<Props, Store, Component, CreateJSX> {
|
||||||
render: (props: Props) => {
|
render: (props: Props) => Promise<{
|
||||||
component: TestContainer
|
component: TestContainer
|
||||||
node: Element
|
node: Element
|
||||||
waitForActions(timeout?: number): Promise<void>
|
waitForActions(timeout?: number): Promise<void>
|
||||||
}
|
}>
|
||||||
store: Store
|
store: Store
|
||||||
Component: Component
|
Component: Component
|
||||||
withJSX: (localCreateJSX: CreateJSX)
|
withJSX: (localCreateJSX: CreateJSX)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ describe('WaitMiddleware', () => {
|
|||||||
it('waits for certain async actions to be resolved', async () => {
|
it('waits for certain async actions to be resolved', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B', 'C'])
|
const promise = wm.wait({B: 1, C: 1})
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
type: 'A',
|
type: 'A',
|
||||||
@ -40,7 +40,7 @@ describe('WaitMiddleware', () => {
|
|||||||
it('times out when actions do not happen', async () => {
|
it('times out when actions do not happen', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B', 'C'], 5)
|
const promise = wm.wait({B: 1, C: 1}, 5)
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
type: 'A',
|
type: 'A',
|
||||||
@ -58,7 +58,7 @@ describe('WaitMiddleware', () => {
|
|||||||
it('errors out when a promise is rejected', async () => {
|
it('errors out when a promise is rejected', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B', 'C'])
|
const promise = wm.wait({B: 1, C: 1})
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
type: 'A',
|
type: 'A',
|
||||||
@ -76,8 +76,8 @@ describe('WaitMiddleware', () => {
|
|||||||
it('errors out when wait is called twice', async () => {
|
it('errors out when wait is called twice', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B'])
|
const promise = wm.wait({B: 1})
|
||||||
const error = await getError(wm.wait(['B']))
|
const error = await getError(wm.wait({B: 1}))
|
||||||
expect(error.message).toMatch(/already waiting/)
|
expect(error.message).toMatch(/already waiting/)
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: new Error('test'),
|
payload: new Error('test'),
|
||||||
@ -90,7 +90,7 @@ describe('WaitMiddleware', () => {
|
|||||||
it('does nothing when pending is dispatched', async () => {
|
it('does nothing when pending is dispatched', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
const store = getStore(wm)
|
||||||
const promise = wm.wait(['B'], 1)
|
const promise = wm.wait({B: 1}, 1)
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
payload: undefined,
|
payload: undefined,
|
||||||
type: 'B',
|
type: 'B',
|
||||||
@ -102,7 +102,7 @@ describe('WaitMiddleware', () => {
|
|||||||
|
|
||||||
it('resolved immediately when no actions are defined', async () => {
|
it('resolved immediately when no actions are defined', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
await wm.wait([])
|
await wm.wait({})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -129,11 +129,54 @@ describe('WaitMiddleware', () => {
|
|||||||
it('records custom actions', async () => {
|
it('records custom actions', async () => {
|
||||||
const wm = new WaitMiddleware()
|
const wm = new WaitMiddleware()
|
||||||
const store = getStore(wm)
|
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: 'test1'} as any)
|
||||||
store.dispatch({type: 'tes'} as any)
|
store.dispatch({type: 'tes'} as any)
|
||||||
store.dispatch({type: 'test3'} 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.
|
* Starts recording actions and returns the recorder.
|
||||||
*/
|
*/
|
||||||
record(shouldRecord: ShouldRecord = defaultShouldRecord): Recorder {
|
record(
|
||||||
const recorder = new Recorder(shouldRecord)
|
shouldRecord: ShouldRecord = defaultShouldRecord,
|
||||||
|
shouldResolve: ShouldRecord = defaultShouldResolve,
|
||||||
|
shouldReject: ShouldRecord = defaultShouldReject,
|
||||||
|
): Recorder {
|
||||||
|
const recorder = new Recorder(shouldRecord, shouldResolve, shouldReject)
|
||||||
this.recorders.push(recorder)
|
this.recorders.push(recorder)
|
||||||
return recorder
|
return recorder
|
||||||
}
|
}
|
||||||
@ -36,6 +40,10 @@ export class WaitMiddleware {
|
|||||||
*/
|
*/
|
||||||
async waitForRecorded(recorder: Recorder, timeout?: number): Promise<void> {
|
async waitForRecorded(recorder: Recorder, timeout?: number): Promise<void> {
|
||||||
this.stopRecording(recorder)
|
this.stopRecording(recorder)
|
||||||
|
const error = recorder.getError()
|
||||||
|
if (error) {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
await this.wait(recorder.getActionTypes(), timeout)
|
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
|
* Waits for actions to be resolved or rejected. Times out after 10 seconds
|
||||||
* by default.
|
* by default.
|
||||||
*/
|
*/
|
||||||
async wait(actions: string[], timeout = 10000): Promise<void> {
|
async wait(actions: Record<string, number>, timeout = 10000): Promise<void> {
|
||||||
if (this.notify) {
|
if (this.notify) {
|
||||||
throw new Error('WaitMiddleware.wait - already waiting!')
|
throw new Error('WaitMiddleware.wait - already waiting!')
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionsByName = actions.reduce((obj, type) => {
|
let count = Object.keys(actions).reduce((count, type) => {
|
||||||
obj[type] = (obj[type] || 0) + 1
|
return count + actions[type]
|
||||||
return obj
|
}, 0)
|
||||||
}, {} as Record<string, number>)
|
|
||||||
let count = actions.length
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!actions.length) {
|
if (!count) {
|
||||||
resolve()
|
resolve()
|
||||||
this.notify = undefined
|
this.notify = undefined
|
||||||
return
|
return
|
||||||
@ -67,14 +73,14 @@ export class WaitMiddleware {
|
|||||||
}, timeout)
|
}, timeout)
|
||||||
|
|
||||||
this.notify = (action: AsyncAction<unknown, string>) => {
|
this.notify = (action: AsyncAction<unknown, string>) => {
|
||||||
if (!actionsByName[action.type]) {
|
if (!actions[action.type]) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (action.status) {
|
switch (action.status) {
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return
|
return
|
||||||
case 'resolved':
|
case 'resolved':
|
||||||
actionsByName[action.type]--
|
actions[action.type]--
|
||||||
count--
|
count--
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
clearTimeout(t)
|
clearTimeout(t)
|
||||||
@ -100,19 +106,39 @@ export type ShouldRecord = (action: AnyAction) => boolean
|
|||||||
|
|
||||||
export const defaultShouldRecord: ShouldRecord =
|
export const defaultShouldRecord: ShouldRecord =
|
||||||
(action: AnyAction) => 'status' in action && action.status === 'pending'
|
(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 {
|
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[] {
|
getActionTypes() {
|
||||||
return this.actionTypes.slice()
|
return {...this.actionTypes}
|
||||||
|
}
|
||||||
|
|
||||||
|
getError() {
|
||||||
|
return this.error
|
||||||
}
|
}
|
||||||
|
|
||||||
record(action: AnyAction) {
|
record(action: AnyAction) {
|
||||||
if (this.shouldRecord(action)) {
|
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