[CORL-729] Upgrade Final Form & improve tests (#2735)

* CORL-729 Upgrade final form, fix and improve tests

This is a squashed rebase from these commits:
a300b31c23ab11e5e6f0668bc03ece7697360aaa feat: error on optimisic response warnings during test
dd8a9776865ec41d346e23ae0743d0d4fb0caa21 fix: turn off @typescript-eslint/prefer-regexp-exec rule
b995daacf1722cace60d755e672cb6a3a20d6bc2 feat: mute false warnings in test
e44f9e28307cd63a82c1fb7ac013667dd7b7bc46 fix: wrap remaining tests with act
afbd4329b97f3dbc9f873ea4ff234d98bb651ccf feat: fail when act(async () => ...) without await warnings
51dfb60b7d75411ba2e1a28db33f4aba5cf84de1 feat: fail tests with act warnings
97f93546ed8113e207882411eb4cdb7675b0796c fix: mock window.resizeTo globally in tests
850958b8c4d2fc0aca67ae580296433af223f8ab fix: more tests with act
24c05ab88e9a416e4962acc3f20f2c764ba07657 fix: wrap charCountEditComment test in acts
ed590b82d147470bba74055dc682e6b70d2e76fb fix signInWithEmail
4a2b9402cb6ce9565d99ae1a950eaa422ff603c3 fix: PostCommentFormContainer
815ebe6ef364d954d4bd0a35495934c9d014170b fix: use final form initialize instead of reset and remove obsolete
d3101f2112ed3ffe8d06609620e31e6655d2cf6d Merge branch 'feature/CORL-729-final-form' of https://github.com/coralproject/talk into feature/CORL-729-final-form
a0658da610a5f39b6fae78ffb8dd291b22d54e50 fix: addEmailAddress
60f7fc99a44dfa49dfd401a0ef49c60973b3e8e3 fix: use proper act pattern in renderConfigure.spec.tsx
d66bdfc2245c2b1ee03a1b3a3a56f1d5ba14ddc4 Stop propagation of Modal content click events (#2706)
ec6689594136e22a5b9f05ea284162702dc4955d fix: use proper act pattern in createUsername.spec.tsx
ef239496964a5f9a91ee1c4424ad81537c4f47fe fix: stream configuration
c7e06a0c6aef6b299c41392af81f8a20855028bf fix: user proper act pattern in streamConfiguration
9712e659e394a898500ed649464ff14d4870e589 Merge branch 'feature/CORL-729-final-form' of https://github.com/coralproject/talk into feature/CORL-729-final-form
9e5cfbaf3593615b457055de23f187fa07edd4c4 fix: signIn test
99b44a4a1bbb7ff2cd44c6821ad33b80f90c4a99 fix: user proper act pattern in stories.spec.tsx
ed7c1a92f93ab9aaa85ff92837b0ed21560cb358 fix: user proper act pattern in addEmailAddress
a04b392cb2148b9a24791b062027796c409d053e fix: remove obsolete snapshots
59df67c0f9b9d26c74e2cca7d333f5868b0b202d fix: signUp test
7656f179df95b4cd96b37afbc88a1c3a2944fdf4 fix: use proper act pattern on createPassword
85246fbf1f9ab49ad3a09c11ab79bf537059b548 fix: use correct act() pattern in createUsername.spec.tsx
d5239373a2d1bbed0bfe8c1ca62ef6a70ef5c7d9 fix: the correct way to use act on form submit :-)
d84ecd168354f4acb422a5ddb725fb8faf9c3184 fix: moderation test
d8df62ab1a6486144684ff917c47e6e375ffbe03 fix: reportComment test
2756e3184bb292168e8d34e201f340c3799941e6 fix: auth tests
a28695dbdd313a7bc3dade9ac1f92d6ef0061526 Properly handle final-form actions in tests
2fafc8ea3458c5b15b66f3d65f0947672dd1a516 Update snapshots now that final form isn't overwriting props
1f9bbaec8678a7653124898ba4a2e84ddc1ef243 fix: prevent final form from overwriting field props
f6c66c003d1917db2dcb3f757e8a303266c381fa fix: prevent final form fields overwrite out props
48d1fc7318ee4ba7bf72839127e9a0b1487c1729 chore: rename translation string
728373da5728a4e7c039bd0c3a3cf0037e9f5177 fix: purge relay metadata from request
7cdea925087a6b9b6e318bbb1b31b798be87dc2f fix: radiobuttons
9735bae79222219a81a28d458976a596201b650b chore: revert obsolete checks
0b556e1693584430a5814e81d87b0f233efd1a30 fix: refactor admin configure
b245afc7b196035bcb454e031c966e63c77ce522 fix: implement withForm HOC
5787400051211f5d2e1773d7207f32b66b02a2a1 Update the Configure page form state to properly load form values
8c2af3e22a96a3d2e50e7f06fb45d1fb79cf0c8e Replace form.reset() with setTimeout(form.reset)
27d9c90e3f0166cc2db45db461619be15a3cb950 Update radio buttons and on/off fields to work with final-form updates
b852dd14af85b14ff8e0d2823e1e83bf278b29b9 Replace any on OnSubmit with typed form submission interfaces
f049a70aaf4872825ac3b2aa62dc5cb7f945290a (f) Preliminarily get Coral compiling with latest final-form libraries

* feat: act is now actAndReturn

* fix: print original filename and line number in custom console impl

* feat: trace process warnings

* fix: server warnings about potentially memory leak with too many listeners
This commit is contained in:
Vinh
2019-12-03 01:07:50 +07:00
committed by GitHub
parent 4ee8cf7c96
commit bc27d7fbec
239 changed files with 4993 additions and 10263 deletions
@@ -1,40 +0,0 @@
import { FormApi } from "final-form";
import { merge } from "lodash";
import React from "react";
interface Props {
form: FormApi;
rootKey?: string;
children: (params: { onInitValues: (data: any) => void }) => React.ReactNode;
}
/**
* FormInitializer exposes `onInitValues` property as a render prop.
* This prop can be called multiple times until `FormInitializer` will
* run `form.initizalize` during mount.
*
* This is useful if you're initialization data is spread across components.
*/
class FormInitializer extends React.Component<Props> {
private initialValues: any = {};
public componentDidMount() {
let values = this.initialValues;
if (this.props.rootKey) {
values = { [this.props.rootKey]: values };
}
this.props.form.initialize(values);
}
private handlePartialInit = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
};
public render() {
return this.props.children({
onInitValues: this.handlePartialInit,
});
}
}
export default FormInitializer;
@@ -1,2 +1 @@
export { default as FormInitializer } from "./FormInitializer";
export { default as ValidationMessage } from "./ValidationMessage";
+15 -1
View File
@@ -12,6 +12,10 @@ export type OnSubmit<T> = (
form: FormApi
) => ErrorsObject<T> | Promise<ErrorsObject<T> | void> | void;
export interface FormError {
"FINAL_FORM/form-error": any;
}
export const parseEmptyAsNull = (v: any) => {
if (v === "") {
return null;
@@ -47,12 +51,22 @@ export const formatPercentage = (v: any) => {
export const parseBool = (v: any) => Boolean(v);
export const parseStringBool = (v: string) => v === "true";
export const formatBool = (v: boolean) => {
return v ? "true" : "false";
};
export const parseNewLineDelimitedString = (v: string) => v.split("\n");
export const formatNewLineDelimitedString = (
v: ReadonlyArray<string> | undefined | null
) => (v && v.join("\n")) || "";
export const parseString = (v: string | undefined) => {
if (!v) {
return "";
}
return v;
};
export const parseStringList = (v: string) => {
if (v === "") {
return [];
@@ -68,7 +82,7 @@ export const formatStringList = (v: string[] | null) => {
};
export type FieldMeta = Pick<
FieldRenderProps["meta"],
FieldRenderProps<any, HTMLElement>["meta"],
"touched" | "error" | "submitError"
>;
@@ -3,3 +3,4 @@ export * from "./helpers";
export * from "./SubmitHookContext";
export { default as withSubmitHookContext } from "./withSubmitHookContext";
export { default as SubmitHookHandler } from "./SubmitHookHandler";
export { default as withForm } from "./withForm";
@@ -0,0 +1,24 @@
import { FormApi } from "final-form";
import React from "react";
import { useForm } from "react-final-form";
import {
hoistStatics,
InferableComponentEnhancer,
wrapDisplayName,
} from "recompose";
interface FormProps {
form: FormApi;
}
const withForm: InferableComponentEnhancer<FormProps> = hoistStatics<any>(
(WrappedComponent: React.ComponentType<any>) => {
const Component: React.FunctionComponent<any> = props => {
const form = useForm<any>();
return <WrappedComponent {...props} form={form} />;
};
Component.displayName = wrapDisplayName(WrappedComponent, "withForm");
return Component;
}
);
export default withForm;
@@ -51,3 +51,4 @@ export {
withSubscription,
combineDisposables,
} from "./subscription";
export { default as purgeMetadata } from "./purgeMetadata";
@@ -0,0 +1,44 @@
import purgeMetadata from "./purgeMetadata";
it("purges metadata", () => {
const A = {
a: {
s: "t",
v: null,
__fragments: "",
},
b: {
d: {
f: 4,
__fragments: "",
},
},
c: [
"test",
{
k: 4,
__fragments: "",
},
],
x: "yz",
};
const B = {
a: {
s: "t",
v: null,
},
b: {
d: {
f: 4,
},
},
c: [
"test",
{
k: 4,
},
],
x: "yz",
};
expect(purgeMetadata(A)).toEqual(B);
});
@@ -0,0 +1,26 @@
/**
* Purges relay metadata from response e.g. information about fragments.
*
* @param data Relay response data
*/
export default function purgeMetadata<T>(data: T): T {
switch (typeof data) {
case "object": {
if (data === null) {
return data;
}
if (Array.isArray(data)) {
return data.map(purgeMetadata) as any;
}
const result: any = {};
Object.keys(data)
.filter(k => !k.startsWith("__"))
.forEach(k => {
result[k] = purgeMetadata((data as any)[k]);
});
return result;
}
default:
return data;
}
}
+19 -4
View File
@@ -1,7 +1,22 @@
import TestRenderer from "react-test-renderer";
export default function act(
callback: () => Promise<void> | void | undefined
): Promise<void> | void {
return TestRenderer.act(callback as any) as any;
import { isPromiseLike } from "coral-common/utils";
export default function act<T>(callback: () => T): T {
let callbackResult: T;
const actResult = TestRenderer.act(() => {
callbackResult = callback();
return callbackResult as any;
});
if (isPromiseLike(callbackResult!)) {
// Return it this way, to preserve warnings that React emits.
return {
then(resolve: (value: any) => void, reject: (err?: Error) => void) {
(actResult as PromiseLike<any>).then(() => {
resolve(callbackResult);
}, reject);
},
} as any;
}
return callbackResult!;
}
@@ -1,5 +1,6 @@
import { ReactTestInstance } from "react-test-renderer";
import findParent from "./findParent";
import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText";
const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
@@ -15,6 +16,18 @@ const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
});
};
export function getParentByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
const result = findParent(container, matcher(pattern, options));
if (!result) {
throw new Error(`Parent with pattern ${pattern} not found`);
}
return result;
}
export function getByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
@@ -47,6 +60,14 @@ export function queryByType(
return results[0];
}
export function queryParentByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
options?: TextMatchOptions
) {
return findParent(container, matcher(pattern, options));
}
export function queryAllByType(
container: ReactTestInstance,
pattern: TextMatchPattern,
@@ -0,0 +1,14 @@
import { ReactTestInstance } from "react-test-renderer";
export default function findParent(
instance: ReactTestInstance,
matcher: (i: ReactTestInstance) => boolean
): ReactTestInstance | null {
if (matcher(instance)) {
return instance;
}
if (instance.parent) {
return findParent(instance.parent, matcher);
}
return null;
}
@@ -20,8 +20,9 @@ export { default as matchText } from "./matchText";
export { default as toJSON } from "./toJSON";
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as createAccessToken } from "./createAccessToken";
export { default as findParentsWithType } from "./findParentsWithType";
export { default as findParent } from "./findParent";
export { default as findParentWithType } from "./findParentWithType";
export { default as findParentsWithType } from "./findParentsWithType";
export {
default as createFixture,
Fixture,
@@ -14,7 +14,14 @@ import {
queryByTestID,
} from "./byTestID";
import { getAllByText, getByText, queryAllByText, queryByText } from "./byText";
import { getAllByType, getByType, queryAllByType, queryByType } from "./byType";
import {
getAllByType,
getByType,
getParentByType,
queryAllByType,
queryByType,
queryParentByType,
} from "./byType";
import toJSON from "./toJSON";
type Func0<R> = () => R;
@@ -56,8 +63,10 @@ export default function within(container: ReactTestInstance) {
queryByLabelText: applyContainer(container, queryByLabelText),
queryAllByLabelText: applyContainer(container, queryAllByLabelText),
getByType: applyContainer(container, getByType),
getParentByType: applyContainer(container, getParentByType),
getAllByType: applyContainer(container, getAllByType),
queryByType: applyContainer(container, queryByType),
queryParentByType: applyContainer(container, queryParentByType),
queryAllByType: applyContainer(container, queryAllByType),
toJSON: () => toJSON(container),
};