diff --git a/src/core/client/framework/testHelpers/createFakePymStorage.ts b/src/core/client/framework/testHelpers/createFakePymStorage.ts index ad8510dcd..f7303411f 100644 --- a/src/core/client/framework/testHelpers/createFakePymStorage.ts +++ b/src/core/client/framework/testHelpers/createFakePymStorage.ts @@ -12,7 +12,7 @@ export class FakeStorage implements PymStorage { return Promise.resolve(); } public getItem(key: string) { - return Promise.resolve(this.store[key]); + return Promise.resolve(this.store[key] || null); } } diff --git a/src/core/client/framework/testHelpers/removeFragmentRefs.ts b/src/core/client/framework/testHelpers/removeFragmentRefs.ts index b92e57aa9..4dd29247b 100644 --- a/src/core/client/framework/testHelpers/removeFragmentRefs.ts +++ b/src/core/client/framework/testHelpers/removeFragmentRefs.ts @@ -1,12 +1,42 @@ import { ComponentType } from "react"; -/** Remove all traces of `$fragmentRefs` and `$refType` from type recursively */ +/** Remove `$fragmentRefs` and `$refType` on a single object */ +export type OmitFragments = Pick< + T, + { + [P in keyof T]: P extends " $fragmentRefs" | " $refType" ? never : P + }[keyof T] +>; + export type NoFragmentRefs = T extends object - ? { - [P in Exclude]: NoFragmentRefs< - T[P] - > - } + ? T extends ((...args: any[]) => any) + ? T + : T extends ReadonlyArray + ? ReadonlyArray> // TODO: (cvle) this should normally reference itself but it complains about a circular reference. + : { [P in keyof OmitFragments]: NoFragmentRefs } + : T; + +// TODO: (cvle) these NoFragmentRefX are a workaround for above issue +export type NoFragmentRefs2 = T extends object + ? T extends ((...args: any[]) => any) + ? T + : T extends ReadonlyArray + ? ReadonlyArray> + : { [P in keyof OmitFragments]: NoFragmentRefs } + : T; + +export type NoFragmentRefs3 = T extends object + ? T extends ((...args: any[]) => any) + ? T + : T extends ReadonlyArray + ? ReadonlyArray> + : { [P in keyof OmitFragments]: NoFragmentRefs } + : T; + +export type NoFragmentRefs4 = T extends object + ? T extends ((...args: any[]) => any) + ? T + : { [P in keyof OmitFragments]: NoFragmentRefs } : T; export default function removeFragmentRefs( diff --git a/src/core/client/stream/components/Comment/IndentedComment.spec.tsx b/src/core/client/stream/components/Comment/IndentedComment.spec.tsx new file mode 100644 index 000000000..c86a66ef2 --- /dev/null +++ b/src/core/client/stream/components/Comment/IndentedComment.spec.tsx @@ -0,0 +1,20 @@ +import { shallow } from "enzyme"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import IndentedComment from "./IndentedComment"; + +it("renders correctly", () => { + const props: PropTypesOf = { + indentLevel: 1, + id: "comment-id", + author: { + username: "Marvin", + }, + body: "Woof", + createdAt: "1995-12-17T03:24:00.000Z", + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment/ReplyButton.spec.tsx b/src/core/client/stream/components/Comment/ReplyButton.spec.tsx new file mode 100644 index 000000000..f1e2a4267 --- /dev/null +++ b/src/core/client/stream/components/Comment/ReplyButton.spec.tsx @@ -0,0 +1,17 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; + +import { PropTypesOf } from "talk-framework/types"; + +import ReplyButton from "./ReplyButton"; + +it("renders correctly", () => { + const props: PropTypesOf = { + id: "id", + onClick: noop, + active: true, + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); +}); diff --git a/src/core/client/stream/components/Comment/ReplyButton.tsx b/src/core/client/stream/components/Comment/ReplyButton.tsx index 43b1028ca..cf8ddd25b 100644 --- a/src/core/client/stream/components/Comment/ReplyButton.tsx +++ b/src/core/client/stream/components/Comment/ReplyButton.tsx @@ -4,12 +4,14 @@ import React, { EventHandler, MouseEvent, StatelessComponent } from "react"; import { Button, ButtonIcon, MatchMedia } from "talk-ui/components"; interface Props { + id?: string; onClick?: EventHandler>; active?: boolean; } const ReplyButton: StatelessComponent = props => ( - - - - - - - - )} - -); + Cancel + + + + + + + + + )} + + ); +}; export default ReplyCommentForm; diff --git a/src/core/client/stream/containers/CommentContainer.tsx b/src/core/client/stream/containers/CommentContainer.tsx index 8624110fd..aed03f7ff 100644 --- a/src/core/client/stream/containers/CommentContainer.tsx +++ b/src/core/client/stream/containers/CommentContainer.tsx @@ -49,6 +49,7 @@ export class CommentContainer extends Component { footer={ <> diff --git a/src/core/client/stream/containers/ReplyCommentFormContainer.spec.tsx b/src/core/client/stream/containers/ReplyCommentFormContainer.spec.tsx new file mode 100644 index 000000000..0f437cb17 --- /dev/null +++ b/src/core/client/stream/containers/ReplyCommentFormContainer.spec.tsx @@ -0,0 +1,171 @@ +import { shallow } from "enzyme"; +import { noop } from "lodash"; +import React from "react"; +import sinon from "sinon"; + +import { PropTypesOf } from "talk-framework/types"; + +import { timeout } from "talk-common/utils"; +import { + createFakePymStorage, + removeFragmentRefs, +} from "talk-framework/testHelpers"; +import { ReplyCommentFormContainer } from "./ReplyCommentFormContainer"; + +const ReplyCommentFormContainerN = removeFragmentRefs( + ReplyCommentFormContainer +); + +function getContextKey(commentID: string) { + return `replyCommentFormBody-${commentID}`; +} + +it("renders correctly", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + }, + pymSessionStorage: createFakePymStorage(), + }; + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it("renders with initialValues", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + }, + pymSessionStorage: createFakePymStorage(), + }; + + await props.pymSessionStorage.setItem( + getContextKey(props.comment.id), + "Hello World!" + ); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + expect(wrapper).toMatchSnapshot(); +}); + +it("save values", async () => { + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + }, + pymSessionStorage: createFakePymStorage(), + }; + + await props.pymSessionStorage.setItem( + getContextKey(props.comment.id), + "Hello World!" + ); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + wrapper + .first() + .props() + .onChange({ values: { body: "changed" } }); + expect( + await props.pymSessionStorage.getItem(getContextKey(props.comment.id)) + ).toBe("changed"); +}); + +it("creates a comment", async () => { + const assetID = "asset-id"; + const input = { body: "Hello World!" }; + const createCommentStub = sinon.stub(); + const form = { reset: noop }; + const onCloseStub = sinon.stub(); + + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: createCommentStub, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + }, + pymSessionStorage: createFakePymStorage(), + onClose: onCloseStub, + }; + + await props.pymSessionStorage.setItem( + getContextKey(props.comment.id), + "Hello World!" + ); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + wrapper + .first() + .props() + .onSubmit(input, form); + expect( + createCommentStub.calledWith({ + assetID, + parentID: props.comment.id, + ...input, + }) + ).toBeTruthy(); + await timeout(); + expect(onCloseStub.calledOnce).toBe(true); +}); + +it("closes on cancel", async () => { + const onCloseStub = sinon.stub(); + const props: PropTypesOf = { + // tslint:disable-next-line:no-empty + createComment: (() => {}) as any, + asset: { + id: "asset-id", + }, + comment: { + id: "comment-id", + }, + pymSessionStorage: createFakePymStorage(), + onClose: onCloseStub, + }; + + await props.pymSessionStorage.setItem( + getContextKey(props.comment.id), + "Hello World!" + ); + + const wrapper = shallow(); + await timeout(); + wrapper.update(); + wrapper.findWhere(w => !!w.prop("onCancel")).prop("onCancel")(); + + // Calls close. + expect(onCloseStub.calledOnce).toBe(true); + + // Removes saved value. + expect( + await props.pymSessionStorage.getItem(getContextKey(props.comment.id)) + ).toBeNull(); +}); diff --git a/src/core/client/stream/containers/ReplyCommentFormContainer.tsx b/src/core/client/stream/containers/ReplyCommentFormContainer.tsx index 0e376226e..84425c1c8 100644 --- a/src/core/client/stream/containers/ReplyCommentFormContainer.tsx +++ b/src/core/client/stream/containers/ReplyCommentFormContainer.tsx @@ -67,6 +67,7 @@ export class ReplyCommentFormContainer extends Component { parentID: this.props.comment.id, ...input, }); + this.props.pymSessionStorage.removeItem(this.contextKey); if (this.props.onClose) { this.props.onClose(); @@ -95,6 +96,7 @@ export class ReplyCommentFormContainer extends Component { } return ( { public state = { disableLoadMore: false, @@ -81,9 +90,7 @@ const enhanced = withPaginationContainer< @connection(key: "Stream_comments") { edges { node { - id - ...CommentContainer_comment - ...ReplyListContainer_comment + ...StreamContainer_comment @relay(mask: false) } } } diff --git a/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap b/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap index 85fc8463c..f7bd9ae74 100644 --- a/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap +++ b/src/core/client/stream/containers/__snapshots__/CommentContainer.spec.tsx.snap @@ -14,6 +14,7 @@ exports[`renders body only 1`] = ` +`; + +exports[`renders with initialValues 1`] = ` + +`; diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index 95c874ff7..670784753 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -22,13 +22,7 @@ const mutation = graphql` edge { cursor node { - id - author { - id - username - } - body - createdAt + ...StreamContainer_comment @relay(mask: false) } } clientMutationId @@ -98,7 +92,7 @@ function commit(environment: Environment, input: CreateCommentInput) { }, clientMutationId: (clientMutationId++).toString(), }, - }, + } as any, // TODO: (cvle) generated types should contain one for the optimistic response. configs: getConfig(input), }); } diff --git a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap index 955e252f0..c72a5212a 100644 --- a/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/loadMore.spec.tsx.snap @@ -181,6 +181,7 @@ exports[`loads more comments 1`] = ` > + + + +
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+ + Markus + + +
+
+
+ +
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+ + +
+
+ +
+
+
+
+ + Lukas + + +
+
+
+ +
+
+
+
+
+
+`; + +exports[`post a reply 2`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+ + Markus + + +
+
+
+ +
+
+
+
+
+ +
+
+
+ + + +
+
Hello world!", + } + } + id="comments-replyCommentForm-rte-comment-0" + onBlur={[Function]} + onChange={[Function]} + onCut={[Function]} + onFocus={[Function]} + onInput={[Function]} + onKeyDown={[Function]} + onPaste={[Function]} + onSelect={[Function]} + /> +
+
+
+
+ + +
+
+ +
+
+
+
+ + Markus + + +
+
Hello world!", + } + } + /> +
+ +
+
+
+
+
+
+
+
+ + Lukas + + +
+
+
+ +
+
+
+
+
+
+`; + +exports[`post a reply 3`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+ + Markus + + +
+
+
+ +
+
+
+
+
+
+ + Markus + + +
+
Hello world! (from server)", + } + } + /> +
+ +
+
+
+
+
+
+
+
+ + Lukas + + +
+
+
+ +
+
+
+
+
+
+`; + +exports[`renders comment stream 1`] = ` +
+
+
+
+
+
+ Signed in as + + Markus + + . +
+
+ + Not you?  + + +
+
+
+
+
+
+ +
+
+
+ + + +
+ +
+
+
+
+
+
+ + Powered by + + ⁨The Coral Project⁩ + + +
+ +
+
+ +
+
+
+
+
+ + Markus + + +
+
+
+ +
+
+
+
+
+
+ + Lukas + + +
+
+
+ +
+
+
+
+
+
+`; diff --git a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap index 30b74fa08..684fbfe59 100644 --- a/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/renderReplies.spec.tsx.snap @@ -181,6 +181,7 @@ exports[`renders comment stream 1`] = ` >