diff --git a/TODO.md b/TODO.md index 18b3d63..716e6d0 100644 --- a/TODO.md +++ b/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 diff --git a/packages/client/src/components/Link.test.tsx b/packages/client/src/components/Link.test.tsx index aa1bacc..203405e 100644 --- a/packages/client/src/components/Link.test.tsx +++ b/packages/client/src/components/Link.test.tsx @@ -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( { ) } - 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', diff --git a/packages/client/src/components/Modal.test.tsx b/packages/client/src/components/Modal.test.tsx index c362edb..e2e695b 100644 --- a/packages/client/src/components/Modal.test.tsx +++ b/packages/client/src/components/Modal.test.tsx @@ -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' diff --git a/packages/client/src/crumbs/Crumbs.test.tsx b/packages/client/src/crumbs/Crumbs.test.tsx index 6078b0a..dc0226e 100644 --- a/packages/client/src/crumbs/Crumbs.test.tsx +++ b/packages/client/src/crumbs/Crumbs.test.tsx @@ -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( { .withJSX(Component => ) 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/) }) }) diff --git a/packages/client/src/login/configureLogin.test.tsx b/packages/client/src/login/configureLogin.test.tsx index 347096d..a9d5f57 100644 --- a/packages/client/src/login/configureLogin.test.tsx +++ b/packages/client/src/login/configureLogin.test.tsx @@ -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( diff --git a/packages/client/src/login/configureRegister.test.tsx b/packages/client/src/login/configureRegister.test.tsx index fa80d16..1d6e9ef 100644 --- a/packages/client/src/login/configureRegister.test.tsx +++ b/packages/client/src/login/configureRegister.test.tsx @@ -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( diff --git a/packages/client/src/team/configureTeam.test.tsx b/packages/client/src/team/configureTeam.test.tsx index 4c53475..3cc03cb 100644 --- a/packages/client/src/team/configureTeam.test.tsx +++ b/packages/client/src/team/configureTeam.test.tsx @@ -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, diff --git a/packages/react-captcha/src/Captcha.test.tsx b/packages/react-captcha/src/Captcha.test.tsx index f88bf58..3976d8c 100644 --- a/packages/react-captcha/src/Captcha.test.tsx +++ b/packages/react-captcha/src/Captcha.test.tsx @@ -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( { + + TestUtils.defaultTheme = {} + const tu = new TestUtils() + + const Functional: React.FunctionComponent<{}> = () => fn + class Class extends React.PureComponent<{}> { + render() { + return class + } + } + + describe('render', () => { + it('renders a class component', async () => { + const { node, component } = await tu.render() + 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() + 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(text) + expect(node).toBeTruthy() + expect(node.tagName).toBe('SPAN') + expect(node.innerHTML).toBe('text') + expect(component).toBeTruthy() + }) + }) + +}) diff --git a/packages/react-test/src/index.tsx b/packages/react-test/src/index.tsx index af419df..edb43ca 100644 --- a/packages/react-test/src/index.tsx +++ b/packages/react-test/src/index.tsx @@ -15,7 +15,11 @@ interface RenderParams { export class TestContainer extends React.Component<{}> { ref = React.createRef() render() { - return
{this.props.children}
+ return ( +
+ {this.props.children} +
+ ) } } @@ -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( - - - {jsx} - - , - $div, - ) as unknown as TestContainer - const node = component.ref.current!.children[0] + const component: TestContainer | null = await new Promise(resolve => { + ReactDOM.render( + resolve(instance)}> + + {jsx} + + , + $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) : - const result = this.render( + const result = await this.render( {jsx} , @@ -132,11 +140,11 @@ export class TestUtils { } interface Self { - render: (props: Props) => { + render: (props: Props) => Promise<{ component: TestContainer node: Element waitForActions(timeout?: number): Promise - } + }> store: Store Component: Component withJSX: (localCreateJSX: CreateJSX) diff --git a/packages/redux/src/middleware/WaitMiddleware.test.ts b/packages/redux/src/middleware/WaitMiddleware.test.ts index f3585f6..a28bef3 100644 --- a/packages/redux/src/middleware/WaitMiddleware.test.ts +++ b/packages/redux/src/middleware/WaitMiddleware.test.ts @@ -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) }) }) diff --git a/packages/redux/src/middleware/WaitMiddleware.ts b/packages/redux/src/middleware/WaitMiddleware.ts index 831edd8..cdb554c 100644 --- a/packages/redux/src/middleware/WaitMiddleware.ts +++ b/packages/redux/src/middleware/WaitMiddleware.ts @@ -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 { 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 { + async wait(actions: Record, timeout = 10000): Promise { 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) - 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) => { - 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 = {} + 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 } } }