diff --git a/src/core/client/admin/mutations/BanUserMutation.ts b/src/core/client/admin/mutations/BanUserMutation.ts index f38c3fc9b..40f754801 100644 --- a/src/core/client/admin/mutations/BanUserMutation.ts +++ b/src/core/client/admin/mutations/BanUserMutation.ts @@ -46,7 +46,7 @@ const BanUserMutation = createMutation( current: lookup( environment, input.userID - )!.status!.current!.concat([GQLUSER_STATUS.BANNED]), + )!.status.current.concat([GQLUSER_STATUS.BANNED]), ban: { active: true, }, diff --git a/src/core/client/admin/mutations/RemoveUserBanMutation.ts b/src/core/client/admin/mutations/RemoveUserBanMutation.ts index c4f79e4e7..c0358a668 100644 --- a/src/core/client/admin/mutations/RemoveUserBanMutation.ts +++ b/src/core/client/admin/mutations/RemoveUserBanMutation.ts @@ -46,7 +46,7 @@ const RemoveUserBanMutation = createMutation( current: lookup( environment, input.userID - )!.status!.current!.filter(s => s !== GQLUSER_STATUS.BANNED), + )!.status.current.filter(s => s !== GQLUSER_STATUS.BANNED), ban: { active: false, }, diff --git a/src/core/client/admin/routes/configure/sections/advanced/components/CustomCSSConfig.tsx b/src/core/client/admin/routes/configure/sections/advanced/components/CustomCSSConfig.tsx index 4c9b2eb52..059f294c6 100644 --- a/src/core/client/admin/routes/configure/sections/advanced/components/CustomCSSConfig.tsx +++ b/src/core/client/admin/routes/configure/sections/advanced/components/CustomCSSConfig.tsx @@ -2,6 +2,7 @@ import { Localized } from "fluent-react/compat"; import React, { StatelessComponent } from "react"; import { Field } from "react-final-form"; +import { formatEmpty, parseEmptyAsNull } from "talk-framework/lib/form"; import { FormField, HorizontalGutter, @@ -33,7 +34,7 @@ const CustomCSSConfig: StatelessComponent = ({ disabled }) => ( styles. Can be internal or external. - + {({ input, meta }) => ( <> { replaceHistoryLocation("http://localhost/admin/community"); }); @@ -43,7 +45,7 @@ const createTestRenderer = async ( expectAndFail(variables.role).toBeFalsy(); return communityUsers; }, - viewer: () => users.admins[0], + viewer: () => viewer, }, }), params.resolvers @@ -113,7 +115,6 @@ it("filter by role", async () => { }); it("can't change viewer role", async () => { - const viewer = users.admins[0]; const { container } = await createTestRenderer(); const viewerRow = within(container).getByText(viewer.username!, { @@ -169,11 +170,11 @@ it("change user role", async () => { }); it("can't change role as a moderator", async () => { - const viewer = users.moderators[0]; + const moderator = users.moderators[0]; const { container } = await createTestRenderer({ resolvers: createResolversStub({ Query: { - viewer: () => viewer, + viewer: () => moderator, }, }), }); @@ -190,8 +191,8 @@ it("load more", async () => { return { edges: [ { - node: users.admins[0], - cursor: users.admins[0].createdAt, + node: viewer, + cursor: viewer.createdAt, }, { node: users.commenters[0], diff --git a/src/core/client/admin/test/configure/advanced.spec.tsx b/src/core/client/admin/test/configure/advanced.spec.tsx index 839a4601a..67d7de925 100644 --- a/src/core/client/admin/test/configure/advanced.spec.tsx +++ b/src/core/client/admin/test/configure/advanced.spec.tsx @@ -105,6 +105,43 @@ it("change custom css", async () => { expect(resolvers.Mutation!.updateSettings!.called).toBe(true); }); +it("remove custom css", async () => { + const resolvers = createResolversStub({ + Query: { + settings: () => + pureMerge(settings, { + customCSSURL: "./custom.css", + }), + }, + Mutation: { + updateSettings: ({ variables }) => { + expectAndFail(variables.settings.customCSSURL).toBeNull(); + return { + settings: pureMerge(settings, variables.settings), + }; + }, + }, + }); + const { configureContainer, advancedContainer } = await createTestRenderer({ + resolvers, + }); + + const customCSSField = within(advancedContainer).getByLabelText("Custom CSS"); + + // Let's change the customCSS field. + customCSSField.props.onChange(""); + + // Send form + within(configureContainer) + .getByType("form") + .props.onSubmit(); + + // Wait for submission to be finished + await wait(() => { + expect(resolvers.Mutation!.updateSettings!.called).toBe(true); + }); +}); + it("change permitted domains to be empty", async () => { const resolvers = createResolversStub({ Mutation: { diff --git a/src/core/client/framework/helpers/getStory.ts b/src/core/client/framework/helpers/getStory.ts deleted file mode 100644 index 170578cf5..000000000 --- a/src/core/client/framework/helpers/getStory.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Environment } from "relay-runtime"; - -export default function getStory(environment: Environment, id: string) { - return environment - .getStore() - .getSource() - .get(id); -} diff --git a/src/core/client/framework/helpers/getStorySettings.ts b/src/core/client/framework/helpers/getStorySettings.ts deleted file mode 100644 index cae16c86b..000000000 --- a/src/core/client/framework/helpers/getStorySettings.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Environment } from "relay-runtime"; - -import getStory from "./getStory"; - -export default function getStorySettings(environment: Environment, id: string) { - const story = getStory(environment, id); - if (!story) { - return null; - } - const storySettingsRef = story.settings.__ref; - return environment - .getStore() - .getSource() - .get(storySettingsRef); -} diff --git a/src/core/client/framework/helpers/getViewer.ts b/src/core/client/framework/helpers/getViewer.ts index 25336ce19..02b4ea824 100644 --- a/src/core/client/framework/helpers/getViewer.ts +++ b/src/core/client/framework/helpers/getViewer.ts @@ -1,12 +1,13 @@ import { Environment } from "relay-runtime"; +import { lookup } from "talk-framework/lib/relay"; +import { GQLUser } from "talk-framework/schema"; import getViewerSourceID from "./getViewerSourceID"; export default function getViewer(environment: Environment) { - const source = environment.getStore().getSource(); const viewerID = getViewerSourceID(environment); if (!viewerID) { return null; } - return source.get(viewerID)!; + return lookup(environment, viewerID)!; } diff --git a/src/core/client/framework/helpers/index.ts b/src/core/client/framework/helpers/index.ts index 7493a4951..1b4d3a6dc 100644 --- a/src/core/client/framework/helpers/index.ts +++ b/src/core/client/framework/helpers/index.ts @@ -7,5 +7,3 @@ export { default as redirectOAuth2 } from "./redirectOAuth2"; export { default as getParamsFromHashAndClearIt, } from "./getParamsFromHashAndClearIt"; -export { default as getStory } from "./getStory"; -export { default as getStorySettings } from "./getStorySettings"; diff --git a/src/core/client/framework/lib/relay/lookup.ts b/src/core/client/framework/lib/relay/lookup.ts index 80da6a88e..4e6a81865 100644 --- a/src/core/client/framework/lib/relay/lookup.ts +++ b/src/core/client/framework/lib/relay/lookup.ts @@ -6,7 +6,7 @@ import { Environment, RelayInMemoryRecordSource } from "relay-runtime"; */ type RecordSourceProxy = T extends object ? { - readonly [P in keyof T]?: T[P] extends Array + readonly [P in keyof T]: T[P] extends Array ? ReadonlyArray> : T[P] extends ReadonlyArray ? ReadonlyArray> diff --git a/src/core/client/framework/testHelpers/denormalize.ts b/src/core/client/framework/testHelpers/denormalize.ts index 5f0c76bd1..5af66a39f 100644 --- a/src/core/client/framework/testHelpers/denormalize.ts +++ b/src/core/client/framework/testHelpers/denormalize.ts @@ -1,33 +1,48 @@ -export function denormalizeComment(comment: any, parents: any[] = []) { - const replyNodes = +import { GQLComment, GQLCommentEdge, GQLStory } from "talk-framework/schema"; + +import createFixture, { Fixture } from "./createFixture"; + +export function denormalizeComment( + comment: Fixture, + parents: Array> = [] +): GQLComment { + const replyEdges = (comment.replies && - comment.replies.edges.map((edge: any) => - denormalizeComment(edge, [...parents, comment]) - )) || + comment.replies.edges && + comment.replies.edges.map(edge => ({ + ...edge, + node: + edge.node && + denormalizeComment(edge.node, [ + ...parents, + { node: comment, cursor: comment.createdAt }, + ]), + }))) || []; const repliesPageInfo = (comment.replies && comment.replies.pageInfo) || { endCursor: null, hasNextPage: false, }; - return { + return createFixture({ ...comment, - replies: { edges: replyNodes, pageInfo: repliesPageInfo }, - replyCount: replyNodes.length, + replies: { edges: replyEdges, pageInfo: repliesPageInfo }, + replyCount: replyEdges.length, parentCount: parents.length, parents: { edges: parents, pageInfo: { startCursor: null, hasPreviousPage: false }, }, - }; + }); } -export function denormalizeComments(commentList: any[]) { +export function denormalizeComments(commentList: Array>) { return commentList.map(c => denormalizeComment(c)); } -export function denormalizeStory(story: any) { +export function denormalizeStory(story: Fixture) { const commentNodes = (story.comments && + story.comments.edges && story.comments.edges.map((edge: any) => ({ ...edge, node: denormalizeComment(edge.node), @@ -46,6 +61,6 @@ export function denormalizeStory(story: any) { }; } -export function denormalizeStories(storyList: any[]) { +export function denormalizeStories(storyList: Array>) { return storyList.map(a => denormalizeStory(a)); } diff --git a/src/core/client/stream/mutations/CreateCommentMutation.ts b/src/core/client/stream/mutations/CreateCommentMutation.ts index 1d4ba4065..d974771f8 100644 --- a/src/core/client/stream/mutations/CreateCommentMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentMutation.ts @@ -6,15 +6,16 @@ import { RecordSourceSelectorProxy, } from "relay-runtime"; -import { getStorySettings, getViewer } from "talk-framework/helpers"; +import { getViewer } from "talk-framework/helpers"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitMutationPromiseNormalized, createMutationContainer, + lookup, MutationInput, MutationResponsePromise, } from "talk-framework/lib/relay"; -import { GQLUSER_ROLE } from "talk-framework/schema"; +import { GQLStory, GQLUSER_ROLE } from "talk-framework/schema"; import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql"; import { @@ -111,7 +112,8 @@ function commit( const currentDate = new Date().toISOString(); const id = uuidGenerator(); - const storySettings = getStorySettings(relayEnvironment, input.storyID); + const storySettings = lookup(relayEnvironment, input.storyID)! + .settings; if (!storySettings || !storySettings.moderation) { throw new Error("Moderation mode of the story was not included"); } diff --git a/src/core/client/stream/mutations/CreateCommentReplyMutation.ts b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts index 023d49123..f87458ba4 100644 --- a/src/core/client/stream/mutations/CreateCommentReplyMutation.ts +++ b/src/core/client/stream/mutations/CreateCommentReplyMutation.ts @@ -6,15 +6,16 @@ import { RecordSourceSelectorProxy, } from "relay-runtime"; -import { getStorySettings, getViewer } from "talk-framework/helpers"; +import { getViewer } from "talk-framework/helpers"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { commitMutationPromiseNormalized, createMutationContainer, + lookup, MutationInput, MutationResponsePromise, } from "talk-framework/lib/relay"; -import { GQLUSER_ROLE } from "talk-framework/schema"; +import { GQLStory, GQLUSER_ROLE } from "talk-framework/schema"; import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql"; import { @@ -137,8 +138,8 @@ function commit( const viewer = getViewer(environment)!; const currentDate = new Date().toISOString(); const id = uuidGenerator(); - - const storySettings = getStorySettings(relayEnvironment, input.storyID); + const storySettings = lookup(relayEnvironment, input.storyID)! + .settings; if (!storySettings || !storySettings.moderation) { throw new Error("Moderation mode of the story was not included"); } diff --git a/src/core/client/stream/tabs/comments/components/CommunityGuidelines.tsx b/src/core/client/stream/tabs/comments/components/CommunityGuidelines.tsx index a9470425f..25430b6a2 100644 --- a/src/core/client/stream/tabs/comments/components/CommunityGuidelines.tsx +++ b/src/core/client/stream/tabs/comments/components/CommunityGuidelines.tsx @@ -9,7 +9,7 @@ interface Props { const CommunityGuidelines: StatelessComponent = props => { return ( - + {props.children} ); diff --git a/src/core/client/stream/tabs/comments/components/Stream.tsx b/src/core/client/stream/tabs/comments/components/Stream.tsx index 2a1f0e709..34ecce8bd 100644 --- a/src/core/client/stream/tabs/comments/components/Stream.tsx +++ b/src/core/client/stream/tabs/comments/components/Stream.tsx @@ -4,7 +4,7 @@ import { StatelessComponent } from "react"; import { PropTypesOf } from "talk-framework/types"; import UserBoxContainer from "talk-stream/containers/UserBoxContainer"; -import { Button, HorizontalGutter, Spinner } from "talk-ui/components"; +import { Button, Flex, HorizontalGutter, Spinner } from "talk-ui/components"; import CommentContainer from "../containers/CommentContainer"; import CommunityGuidelinesContainer from "../containers/CommunityGuidelinesContainer"; @@ -52,7 +52,11 @@ const Stream: StatelessComponent = props => { {props.comments.length > 0 && ( )} - {props.refetching && } + {props.refetching && ( + + + + )} {!props.refetching && ( **bold** diff --git a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx index 5a7890156..586231a78 100644 --- a/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx +++ b/src/core/client/stream/tabs/comments/containers/CommentContainer.tsx @@ -88,10 +88,10 @@ export class CommentContainer extends Component { ); } - private openReplyDialog = () => { + private toggleReplyDialog = () => { if (this.props.viewer) { this.setState(state => ({ - showReplyDialog: true, + showReplyDialog: !state.showReplyDialog, })); } else { this.props.showAuthPopup({ view: "SIGN_IN" }); @@ -206,7 +206,7 @@ export class CommentContainer extends Component { id={`comments-commentContainer-replyButton-${ comment.id }`} - onClick={this.openReplyDialog} + onClick={this.toggleReplyDialog} active={showReplyDialog} disabled={ settings.disableCommenting.enabled || story.isClosed diff --git a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx index 3ff2369f5..f3b459bbe 100644 --- a/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx +++ b/src/core/client/stream/tabs/comments/queries/StreamQuery.tsx @@ -8,7 +8,7 @@ import { } from "talk-framework/lib/relay"; import { StreamQuery as QueryTypes } from "talk-stream/__generated__/StreamQuery.graphql"; import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql"; -import { Delay, Spinner } from "talk-ui/components"; +import { Delay, Flex, Spinner } from "talk-ui/components"; import StreamContainer from "../containers/StreamContainer"; interface Props { @@ -43,7 +43,9 @@ export const render = ( return ( - + + + ); }; diff --git a/src/core/client/stream/tabs/comments/queries/__snapshots__/StreamQuery.spec.tsx.snap b/src/core/client/stream/tabs/comments/queries/__snapshots__/StreamQuery.spec.tsx.snap index 3957a4ac6..2015249b4 100644 --- a/src/core/client/stream/tabs/comments/queries/__snapshots__/StreamQuery.spec.tsx.snap +++ b/src/core/client/stream/tabs/comments/queries/__snapshots__/StreamQuery.spec.tsx.snap @@ -10,7 +10,11 @@ exports[`renders loading 1`] = ` - + + + `; diff --git a/src/core/client/stream/tabs/comments/views/permalink/containers/PermalinkViewContainer.tsx b/src/core/client/stream/tabs/comments/views/permalink/containers/PermalinkViewContainer.tsx index fa99a60c8..3660ecb56 100644 --- a/src/core/client/stream/tabs/comments/views/permalink/containers/PermalinkViewContainer.tsx +++ b/src/core/client/stream/tabs/comments/views/permalink/containers/PermalinkViewContainer.tsx @@ -68,6 +68,8 @@ const enhanced = withContext(ctx => ({ fragment PermalinkViewContainer_story on Story { ...ConversationThreadContainer_story ...ReplyListContainer1_story + ...CreateCommentMutation_story + ...CreateCommentReplyMutation_story } `, comment: graphql` @@ -82,6 +84,8 @@ const enhanced = withContext(ctx => ({ ...ConversationThreadContainer_viewer ...ReplyListContainer1_viewer ...UserBoxContainer_viewer + ...CreateCommentMutation_viewer + ...CreateCommentReplyMutation_viewer } `, settings: graphql` diff --git a/src/core/client/stream/tabs/configure/components/MessageBoxConfig.tsx b/src/core/client/stream/tabs/configure/components/MessageBoxConfig.tsx index e564de450..bc77e05b8 100644 --- a/src/core/client/stream/tabs/configure/components/MessageBoxConfig.tsx +++ b/src/core/client/stream/tabs/configure/components/MessageBoxConfig.tsx @@ -3,7 +3,11 @@ import React, { StatelessComponent, Suspense } from "react"; import { Field } from "react-final-form"; import { MarkdownEditor } from "talk-framework/components/loadables"; -import { parseBool } from "talk-framework/lib/form"; +import { + formatEmpty, + parseBool, + parseEmptyAsNull, +} from "talk-framework/lib/form"; import { MessageBox, MessageBoxContent, @@ -54,7 +58,11 @@ const MessageBoxConfig: StatelessComponent = ({ disabled }) => ( {input.checked && ( - + {({ input: iconInput }) => ( {({ input: contentInput, meta }) => ( diff --git a/src/core/client/stream/test/comments/__snapshots__/renderCommunityGuidelines.spec.tsx.snap b/src/core/client/stream/test/comments/__snapshots__/renderCommunityGuidelines.spec.tsx.snap index f397962a0..088012c9b 100644 --- a/src/core/client/stream/test/comments/__snapshots__/renderCommunityGuidelines.spec.tsx.snap +++ b/src/core/client/stream/test/comments/__snapshots__/renderCommunityGuidelines.spec.tsx.snap @@ -40,7 +40,7 @@ exports[`renders comment stream with community guidelines 1`] = `
= {} +) => { + const { testRenderer, context } = create({ + ...params, + resolvers: pureMerge( + createResolversStub({ + Query: { + settings: () => settings, + viewer: () => commenters[0], + comment: ({ variables }) => { + expectAndFail(variables.id).toBe(commentFixture.id); + return commentFixture; + }, + story: ({ variables }) => { + expectAndFail(variables.id).toBe(storyFixture.id); + return storyFixture; + }, + }, + }), + params.resolvers + ), + initLocalState: (localRecord, source, environment) => { + localRecord.setValue(storyFixture.id, "storyID"); + localRecord.setValue(commentFixture.id, "commentID"); + localRecord.setValue(true, "loggedIn"); + if (params.initLocalState) { + params.initLocalState(localRecord, source, environment); + } + }, + }); + + const comment = await waitForElement(() => + within(testRenderer.root).getByTestID("comment-comment-0") + ); + + // Open reply form. + within(comment) + .getByText("Reply", { selector: "button" }) + .props.onClick(); + + const rte = await waitForElement( + () => + findParentWithType( + within(comment).getByLabelText("Write a reply"), + // We'll use the RTE component here as an exception because the + // jsdom does not support all of what is needed for rendering the + // Rich Text Editor. + RTE + )! + ); + + const form = findParentWithType(rte, "form")!; + return { + testRenderer, + context, + comment, + rte, + form, + }; +}; + +it("post a reply", async () => { + const { testRenderer, rte, form } = await createTestRenderer({ + resolvers: createResolversStub({ + Mutation: { + createCommentReply: ({ variables }) => { + expectAndFail(variables).toMatchObject({ + storyID: storyFixture.id, + parentID: storyFixture.comments.edges[0].node.id, + parentRevisionID: storyFixture.comments.edges[0].node.revision.id, + body: "Hello world!", + }); + return { + edge: { + cursor: "", + node: { + ...baseComment, + id: "comment-x", + author: commenters[0], + body: "Hello world! (from server)", + }, + }, + }; + }, + }, + }), + }); + + // Write reply . + rte.props.onChange({ html: "Hello world!" }); + form.props.onSubmit(); + + const commentReplyList = within(testRenderer.root).getByTestID( + "commentReplyList-comment-0" + ); + + // Test after server response. + await waitForElement(() => + within(commentReplyList).getByText("(from server)", { exact: false }) + ); +}); diff --git a/src/core/client/stream/test/comments/postReply.spec.tsx b/src/core/client/stream/test/comments/postReply.spec.tsx index 866e305c9..f21424837 100644 --- a/src/core/client/stream/test/comments/postReply.spec.tsx +++ b/src/core/client/stream/test/comments/postReply.spec.tsx @@ -18,7 +18,7 @@ import { baseComment, commenters, settings, stories } from "../fixtures"; import create from "./create"; async function createTestRenderer( - resolver: any, + resolver: any = {}, options: { muteNetworkErrors?: boolean } = {} ) { const resolvers = { @@ -50,9 +50,11 @@ async function createTestRenderer( ); // Open reply form. - within(comment) - .getByText("Reply", { selector: "button" }) - .props.onClick(); + const replyButton = within(comment).getByText("Reply", { + selector: "button", + }); + + replyButton.props.onClick(); const rte = await waitForElement( () => @@ -70,11 +72,18 @@ async function createTestRenderer( testRenderer, context, comment, + replyButton, rte, form, }; } +it("hides form when reclicking on the reply button", async () => { + const { comment, replyButton } = await createTestRenderer(); + replyButton.props.onClick(); + expect(within(comment).queryByLabelText("Write a reply")).toBeNull(); +}); + it("post a reply", async () => { const { testRenderer, comment, rte, form } = await createTestRenderer({ Mutation: { diff --git a/src/core/client/stream/test/configure/streamConfiguration.spec.tsx b/src/core/client/stream/test/configure/streamConfiguration.spec.tsx index 4a7dd97d4..cf38fe443 100644 --- a/src/core/client/stream/test/configure/streamConfiguration.spec.tsx +++ b/src/core/client/stream/test/configure/streamConfiguration.spec.tsx @@ -1,9 +1,14 @@ -import { cloneDeep } from "lodash"; -import sinon from "sinon"; - import { pureMerge } from "talk-common/utils"; import { + GQLResolver, + MutationToUpdateStorySettingsResolver, +} from "talk-framework/schema"; +import { + createMutationResolverStub, + createResolversStub, + CreateTestRendererParams, findParentWithType, + replaceHistoryLocation, wait, waitForElement, within, @@ -12,30 +17,36 @@ import { import { moderators, settings, stories } from "../fixtures"; import create from "./create"; -async function createTestRenderer( - resolver: any = {}, - options: { muteNetworkErrors?: boolean; status?: string } = {} -) { - const resolvers = { - Query: { - settings: sinon.stub().returns(settings), - story: sinon.stub().callsFake((_: any, variables: any) => { - expectAndFail(variables).toEqual({ id: stories[0].id, url: null }); - return stories[0]; - }), - viewer: sinon.stub().returns(moderators[0]), - ...resolver.Query, - }, - ...resolver, - }; +const viewer = moderators[0]; +const story = stories[0]; +beforeEach(async () => { + replaceHistoryLocation("http://localhost/admin/community"); +}); + +const createTestRenderer = async ( + params: CreateTestRendererParams = {} +) => { const { testRenderer } = create({ - // Set this to true, to see graphql responses. - logNetwork: false, - muteNetworkErrors: options.muteNetworkErrors, - resolvers, - initLocalState: localRecord => { - localRecord.setValue(stories[0].id, "storyID"); + ...params, + resolvers: pureMerge( + createResolversStub({ + Query: { + settings: () => settings, + story: ({ variables }) => { + expectAndFail(variables).toEqual({ id: story.id, url: null }); + return story; + }, + viewer: () => viewer, + }, + }), + params.resolvers + ), + initLocalState: (localRecord, source, environment) => { + localRecord.setValue(story.id, "storyID"); + if (params.initLocalState) { + params.initLocalState(localRecord, source, environment); + } }, }); @@ -46,24 +57,23 @@ async function createTestRenderer( const form = findParentWithType(applyButton, "form")!; return { testRenderer, tabPane, applyButton, form }; -} +}; it("change premod", async () => { - let storyRecord = cloneDeep(stories[0]); - const updateStorySettingsStub = sinon - .stub() - .callsFake((_: any, data: any) => { - expectAndFail(data.input.settings.moderation).toEqual("PRE"); - storyRecord = pureMerge(storyRecord, { settings: data.input.settings }); - return { - story: storyRecord, - clientMutationId: data.input.clientMutationId, - }; - }); + const updateStorySettingsStub = createMutationResolverStub< + MutationToUpdateStorySettingsResolver + >(({ variables }) => { + expectAndFail(variables.settings.moderation).toEqual("PRE"); + return { + story: pureMerge(story, { settings: variables.settings }), + }; + }); const { form, applyButton } = await createTestRenderer({ - Mutation: { - updateStorySettings: updateStorySettingsStub, - }, + resolvers: createResolversStub({ + Mutation: { + updateStorySettings: updateStorySettingsStub, + }, + }), }); const premodField = within(form).getByLabelText("Enable Pre-Moderation"); @@ -89,21 +99,20 @@ it("change premod", async () => { }); it("change premod links", async () => { - let storyRecord = cloneDeep(stories[0]); - const updateStorySettingsStub = sinon - .stub() - .callsFake((_: any, data: any) => { - expectAndFail(data.input.settings.premodLinksEnable).toEqual(true); - storyRecord = pureMerge(storyRecord, { settings: data.input.settings }); - return { - story: storyRecord, - clientMutationId: data.input.clientMutationId, - }; - }); + const updateStorySettingsStub = createMutationResolverStub< + MutationToUpdateStorySettingsResolver + >(({ variables }) => { + expectAndFail(variables.settings.premodLinksEnable).toEqual(true); + return { + story: pureMerge(story, { settings: variables.settings }), + }; + }); const { form, applyButton } = await createTestRenderer({ - Mutation: { - updateStorySettings: updateStorySettingsStub, - }, + resolvers: createResolversStub({ + Mutation: { + updateStorySettings: updateStorySettingsStub, + }, + }), }); const premodLinksField = within(form).getByLabelText( @@ -131,25 +140,24 @@ it("change premod links", async () => { }); it("change message box", async () => { - let storyRecord = cloneDeep(stories[0]); - const updateStorySettingsStub = sinon - .stub() - .callsFake((_: any, data: any) => { - expectAndFail(data.input.settings.messageBox).toEqual({ - enabled: true, - content: "*What do you think?*", - icon: "question_answer", - }); - storyRecord = pureMerge(storyRecord, { settings: data.input.settings }); - return { - story: storyRecord, - clientMutationId: data.input.clientMutationId, - }; + const updateStorySettingsStub = createMutationResolverStub< + MutationToUpdateStorySettingsResolver + >(({ variables }) => { + expectAndFail(variables.settings.messageBox).toEqual({ + enabled: true, + content: "*What do you think?*", + icon: "question_answer", }); + return { + story: pureMerge(story, { settings: variables.settings }), + }; + }); const { form, applyButton } = await createTestRenderer({ - Mutation: { - updateStorySettings: updateStorySettingsStub, - }, + resolvers: createResolversStub({ + Mutation: { + updateStorySettings: updateStorySettingsStub, + }, + }), }); const enableField = within(form).getByLabelText( @@ -162,9 +170,9 @@ it("change message box", async () => { expect(applyButton.props.disabled).toBe(false); // Select icon - within(form) - .getByLabelText("question_answer") - .props.onChange({ target: { value: "question_answer" } }); + const iconButton = within(form).getByLabelText("question_answer"); + + iconButton.props.onChange({ target: { value: iconButton.props.value } }); // Change content. (await waitForElement(() => @@ -185,3 +193,52 @@ it("change message box", async () => { // Should have successfully sent with server. expect(updateStorySettingsStub.called).toBe(true); }); + +it("remove message icon", async () => { + const updateStorySettingsStub = createMutationResolverStub< + MutationToUpdateStorySettingsResolver + >(({ variables }) => { + expectAndFail(variables.settings.messageBox).toEqual({ + enabled: true, + content: "*What do you think?*", + icon: null, + }); + return { + story: pureMerge(story, { settings: variables.settings }), + }; + }); + const { form, applyButton } = await createTestRenderer({ + resolvers: createResolversStub({ + Query: { + story: () => + pureMerge(story, { + settings: { + messageBox: { + enabled: true, + content: "*What do you think?*", + icon: "question_answer", + }, + }, + }), + }, + Mutation: { + updateStorySettings: updateStorySettingsStub, + }, + }), + }); + + // Select icon + const noIconButton = within(form).getByLabelText("No Icon"); + + noIconButton.props.onChange({ target: { value: noIconButton.props.value } }); + + // Send form + form.props.onSubmit(); + + expect(applyButton.props.disabled).toBe(true); + + // Wait for submission to be finished + await wait(() => { + expect(updateStorySettingsStub.called).toBe(true); + }); +}); diff --git a/src/core/client/stream/test/fixtures.ts b/src/core/client/stream/test/fixtures.ts index 7a925455d..83206b4c9 100644 --- a/src/core/client/stream/test/fixtures.ts +++ b/src/core/client/stream/test/fixtures.ts @@ -1,14 +1,24 @@ -import { GQLUSER_ROLE } from "talk-framework/schema"; import { + GQLComment, + GQLCOMMENT_STATUS, + GQLMODERATION_MODE, + GQLSettings, + GQLStory, + GQLUser, + GQLUSER_ROLE, +} from "talk-framework/schema"; +import { + createFixture, + createFixtures, denormalizeComment, denormalizeComments, denormalizeStories, denormalizeStory, } from "talk-framework/testHelpers"; -export const settings = { +export const settings = createFixture({ id: "settings", - moderation: "POST", + moderation: GQLMODERATION_MODE.POST, premodLinksEnable: false, communityGuidelines: { enabled: false, @@ -21,9 +31,8 @@ export const settings = { closeCommenting: { auto: false, message: "Story is closed", - timeout: null, + timeout: undefined, }, - closedAt: null, auth: { integrations: { facebook: { @@ -71,9 +80,9 @@ export const settings = { charCount: { enabled: false, }, -}; +}); -export const commenters = [ +export const commenters = createFixtures([ { id: "user-0", username: "Markus", @@ -94,15 +103,15 @@ export const commenters = [ username: "Markus", role: GQLUSER_ROLE.COMMENTER, }, -]; +]); -export const baseComment = { +export const baseComment = createFixture({ author: commenters[0], body: "Comment Body", revision: { id: "revision-0", }, - status: "NONE", + status: GQLCOMMENT_STATUS.NONE, createdAt: "2018-07-06T18:24:00.000Z", replies: { edges: [], pageInfo: { endCursor: null, hasNextPage: false } }, replyCount: 0, @@ -116,178 +125,188 @@ export const baseComment = { }, }, tags: [], -}; - -export const comments = denormalizeComments([ - { - ...baseComment, - id: "comment-0", - author: commenters[0], - body: "Joining Too", - }, - { - ...baseComment, - id: "comment-1", - author: commenters[1], - body: "What's up?", - }, - { - ...baseComment, - id: "comment-2", - author: commenters[2], - body: "Hey!", - }, - { - ...baseComment, - id: "comment-3", - author: commenters[2], - body: "Comment Body 3", - }, - { - ...baseComment, - id: "comment-4", - author: commenters[2], - body: "Comment Body 4", - }, - { - ...baseComment, - id: "comment-5", - author: commenters[2], - body: "Comment Body 5", - }, -]); - -export const commentWithReplies = denormalizeComment({ - ...baseComment, - id: "comment-with-replies", - author: commenters[0], - body: "I like yoghurt", - replies: { - edges: [ - { node: comments[3], cursor: comments[3].createdAt }, - { node: comments[4], cursor: comments[4].createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - replyCount: 2, }); -export const commentWithDeepReplies = denormalizeComment({ - ...baseComment, - id: "comment-with-deep-replies", - author: commenters[0], - body: "I like yoghurt", - replies: { - edges: [ - { node: commentWithReplies, cursor: commentWithReplies.createdAt }, - { node: comments[5], cursor: comments[5].createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - replyCount: 2, -}); - -export const commentWithDeepestReplies = denormalizeComment({ - ...baseComment, - id: "comment-with-deepest-replies", - body: "body 0", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ +export const comments = denormalizeComments( + createFixtures( + [ { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: "comment-with-deepest-replies-1", - body: "body 1", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ - { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: "comment-with-deepest-replies-2", - body: "body 2", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ - { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: "comment-with-deepest-replies-3", - body: "body 3", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ - { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: "comment-with-deepest-replies-4", - body: "body 4", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ - { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: "comment-with-deepest-replies-5", - body: "body 5", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [ - { - cursor: baseComment.createdAt, - node: { - ...baseComment, - id: - "comment-with-deepest-replies-6", - body: "body 6", - replyCount: 1, - replies: { - ...baseComment.replies, - edges: [], - }, - }, - }, - ], - }, - }, - }, - ], - }, - }, - }, - ], - }, - }, - }, - ], - }, - }, - }, - ], - }, - }, + id: "comment-0", + author: commenters[0], + body: "Joining Too", + }, + { + id: "comment-1", + author: commenters[1], + body: "What's up?", + }, + { + id: "comment-2", + author: commenters[2], + body: "Hey!", + }, + { + id: "comment-3", + author: commenters[2], + body: "Comment Body 3", + }, + { + id: "comment-4", + author: commenters[2], + body: "Comment Body 4", + }, + { + id: "comment-5", + author: commenters[2], + body: "Comment Body 5", }, ], - }, -}); + baseComment + ) +); -export const baseStory = { +export const commentWithReplies = denormalizeComment( + createFixture( + { + id: "comment-with-replies", + author: commenters[0], + body: "I like yoghurt", + replies: { + edges: [ + { node: comments[3], cursor: comments[3].createdAt }, + { node: comments[4], cursor: comments[4].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + replyCount: 2, + }, + baseComment + ) +); + +export const commentWithDeepReplies = denormalizeComment( + createFixture( + { + id: "comment-with-deep-replies", + author: commenters[0], + body: "I like yoghurt", + replies: { + edges: [ + { node: commentWithReplies, cursor: commentWithReplies.createdAt }, + { node: comments[5], cursor: comments[5].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + replyCount: 2, + }, + baseComment + ) +); + +export const commentWithDeepestReplies = denormalizeComment( + createFixture({ + ...baseComment, + id: "comment-with-deepest-replies", + body: "body 0", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: "comment-with-deepest-replies-1", + body: "body 1", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: "comment-with-deepest-replies-2", + body: "body 2", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: "comment-with-deepest-replies-3", + body: "body 3", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: "comment-with-deepest-replies-4", + body: "body 4", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: + "comment-with-deepest-replies-5", + body: "body 5", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [ + { + cursor: baseComment.createdAt, + node: { + ...baseComment, + id: + "comment-with-deepest-replies-6", + body: "body 6", + replyCount: 1, + replies: { + ...baseComment.replies, + edges: [], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + ], + }, + }) +); + +export const baseStory = createFixture({ metadata: { title: "title", }, @@ -302,140 +321,165 @@ export const baseStory = { totalVisible: 0, }, settings: { - moderation: "POST", + moderation: GQLMODERATION_MODE.POST, premodLinksEnable: false, messageBox: { enabled: false, }, }, -}; +}); -export const moderators = [ +export const moderators = createFixtures([ { id: "me-as-moderator", username: "Moderator", role: GQLUSER_ROLE.MODERATOR, }, -]; - -export const commentsFromStaff = denormalizeComments([ - { - ...baseComment, - id: "comment-from-staff-0", - author: moderators[0], - body: "Joining Too", - tags: [{ name: "Staff" }], - }, ]); -export const stories = denormalizeStories([ - { - ...baseStory, - id: "story-1", - url: "http://localhost/stories/story-1", - comments: { - edges: [ - { node: comments[0], cursor: comments[0].createdAt }, - { node: comments[1], cursor: comments[1].createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, - { - ...baseStory, - id: "story-2", - url: "http://localhost/stories/story-2", - comments: { - edges: [ - { node: comments[2], cursor: comments[2].createdAt }, - { node: comments[3], cursor: comments[3].createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, - { - ...baseStory, - id: "story-3", - url: "http://localhost/stories/story-3", - comments: { - edges: [ - { node: comments[0], cursor: comments[0].createdAt }, - { node: commentsFromStaff[0], cursor: commentsFromStaff[0].createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, - }, -]); - -export const storyWithNoComments = denormalizeStory({ - ...baseStory, - id: "story-with-no-comments", - url: "http://localhost/stories/story-with-no-comments", - comments: { - edges: [], - pageInfo: { - hasNextPage: false, - }, - }, -}); - -export const storyWithReplies = denormalizeStory({ - ...baseStory, - id: "story-with-replies", - url: "http://localhost/stories/story-with-replies", - comments: { - edges: [ - { node: comments[0], cursor: comments[0].createdAt }, - { node: commentWithReplies, cursor: commentWithReplies.createdAt }, - ], - pageInfo: { - hasNextPage: false, - }, - }, -}); - -export const storyWithDeepReplies = denormalizeStory({ - ...baseStory, - id: "story-with-deep-replies", - url: "http://localhost/stories/story-with-replies", - comments: { - edges: [ - { node: comments[0], cursor: comments[0].createdAt }, +export const commentsFromStaff = denormalizeComments( + createFixtures( + [ { - node: commentWithDeepReplies, - cursor: commentWithDeepReplies.createdAt, + id: "comment-from-staff-0", + author: moderators[0], + body: "Joining Too", + tags: [{ name: "Staff" }], }, ], - pageInfo: { - hasNextPage: false, - }, - }, -}); + baseComment + ) +); -export const storyWithDeepestReplies = denormalizeStory({ - ...baseStory, - id: "story-with-deepest-replies", - url: "http://localhost/stories/story-with-replies", - comments: { - edges: [ +export const stories = denormalizeStories( + createFixtures( + [ { - node: commentWithDeepestReplies, - cursor: commentWithDeepestReplies.createdAt, + id: "story-1", + url: "http://localhost/stories/story-1", + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { node: comments[1], cursor: comments[1].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + { + id: "story-2", + url: "http://localhost/stories/story-2", + comments: { + edges: [ + { node: comments[2], cursor: comments[2].createdAt }, + { node: comments[3], cursor: comments[3].createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + { + id: "story-3", + url: "http://localhost/stories/story-3", + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { + node: commentsFromStaff[0], + cursor: commentsFromStaff[0].createdAt, + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, }, ], - pageInfo: { - hasNextPage: false, - }, - }, -}); + baseStory + ) +); -export const viewerWithComments = { +export const storyWithNoComments = denormalizeStory( + createFixture( + { + id: "story-with-no-comments", + url: "http://localhost/stories/story-with-no-comments", + comments: { + edges: [], + pageInfo: { + hasNextPage: false, + }, + }, + }, + baseStory + ) +); + +export const storyWithReplies = denormalizeStory( + createFixture( + { + id: "story-with-replies", + url: "http://localhost/stories/story-with-replies", + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { node: commentWithReplies, cursor: commentWithReplies.createdAt }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + baseStory + ) +); + +export const storyWithDeepReplies = denormalizeStory( + createFixture( + { + id: "story-with-deep-replies", + url: "http://localhost/stories/story-with-replies", + comments: { + edges: [ + { node: comments[0], cursor: comments[0].createdAt }, + { + node: commentWithDeepReplies, + cursor: commentWithDeepReplies.createdAt, + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + baseStory + ) +); + +export const storyWithDeepestReplies = denormalizeStory( + createFixture( + { + id: "story-with-deepest-replies", + url: "http://localhost/stories/story-with-replies", + comments: { + edges: [ + { + node: commentWithDeepestReplies, + cursor: commentWithDeepestReplies.createdAt, + }, + ], + pageInfo: { + hasNextPage: false, + }, + }, + }, + baseStory + ) +); + +export const viewerWithComments = createFixture({ id: "me-with-comments", username: "Markus", role: GQLUSER_ROLE.COMMENTER, @@ -454,4 +498,4 @@ export const viewerWithComments = { hasNextPage: false, }, }, -}; +}); diff --git a/src/core/client/ui/components/TileSelector/TileSelector.tsx b/src/core/client/ui/components/TileSelector/TileSelector.tsx index 1e8d1b8cb..be85c118e 100644 --- a/src/core/client/ui/components/TileSelector/TileSelector.tsx +++ b/src/core/client/ui/components/TileSelector/TileSelector.tsx @@ -26,7 +26,7 @@ const TileSelector: StatelessComponent = props => { const { id, name, value, className, children, onChange } = props; const onItemChange = useCallback( (evt: React.ChangeEvent) => - onChange && onChange(evt.target.value || null), + onChange && onChange(evt.target.value), [onChange] ); return ( diff --git a/src/core/server/models/story/index.ts b/src/core/server/models/story/index.ts index e8d1889aa..f3350cb80 100644 --- a/src/core/server/models/story/index.ts +++ b/src/core/server/models/story/index.ts @@ -1,4 +1,3 @@ -import { isNull, omitBy } from "lodash"; import { Db, MongoError } from "mongodb"; import uuid from "uuid"; @@ -337,7 +336,7 @@ export async function updateStorySettings( // Only update fields that have been updated. const update = { $set: { - ...omitBy(dotize({ settings: input }, { embedArrays: true }), isNull), + ...dotize({ settings: input }, { embedArrays: true }), // Always update the updated at time. updatedAt: now, },