mirror of
https://github.com/wassname/talk.git
synced 2026-07-14 11:18:50 +08:00
[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:
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user