[CORL-239, CORL-128] Support disabled commenting and closed stories in Stream (#2205)

* feat: closed story + disabled commenting

* test: add feature test and fix bugs

* fix: snapshot

* fix: isClosed can't be null

* fix: remove duplicate DeepPartial type

* fix: border color
This commit is contained in:
Kiwi
2019-03-04 23:27:46 +01:00
committed by GitHub
parent 625809b42c
commit 4e043638f6
52 changed files with 1977 additions and 1249 deletions
+1
View File
@@ -1,2 +1,3 @@
// TODO: (@cvle) Extract useful common types into its own package.
export { Omit, Overwrite, PropTypesOf } from "talk-ui/types";
export { DeepPartial } from "talk-common/types";
@@ -0,0 +1,67 @@
import React from "react";
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
import { StoryClosedTimeoutContainer_story as StoryData } from "talk-stream/__generated__/StoryClosedTimeoutContainer_story.graphql";
import {
SetStoryClosedMutation,
withSetStoryClosedMutation,
} from "talk-stream/mutations";
interface Props {
story: StoryData;
setStoryClosed: SetStoryClosedMutation;
}
function createTimeout(callback: () => void, closedAt: string) {
const diff = new Date(closedAt).valueOf() - Date.now();
if (diff > 0) {
return setTimeout(callback, diff);
}
return null;
}
class StoryClosedTimeoutContainer extends React.Component<Props> {
private timer: any = null;
constructor(props: Props) {
super(props);
if (props.story.closedAt) {
this.timer = createTimeout(this.handleClose, this.props.story.closedAt);
}
}
public componentWillReceiveProps(nextProps: Props) {
if (nextProps.story.closedAt !== this.props.story.closedAt) {
clearTimeout(this.timer);
this.timer = createTimeout(this.handleClose, nextProps.story.closedAt);
}
}
public componentWillUnmount() {
clearTimeout(this.timer);
this.timer = null;
}
private handleClose = () => {
this.timer = null;
this.props.setStoryClosed({ storyID: this.props.story.id, isClosed: true });
};
public render() {
return null;
}
}
const enhanced = withSetStoryClosedMutation(
withFragmentContainer<Props>({
story: graphql`
fragment StoryClosedTimeoutContainer_story on Story {
id
closedAt
}
`,
})(StoryClosedTimeoutContainer)
);
export default enhanced;
@@ -4,19 +4,27 @@ import { Environment } from "relay-runtime";
import { createFetchContainer, fetchQuery } from "talk-framework/lib/relay";
import { RefreshSettingsQuery as QueryTypes } from "talk-stream/__generated__/RefreshSettingsQuery.graphql";
export type RefreshSettingsVariables = QueryTypes["variables"];
const query = graphql`
query RefreshSettingsQuery {
query RefreshSettingsQuery($storyID: ID!) {
settings {
...StreamContainer_settings
}
# We also refrech story props that are
# dependent on the settings.
story(id: $storyID) {
closedAt
isClosed
}
}
`;
function fetch(environment: Environment) {
function fetch(environment: Environment, variables: RefreshSettingsVariables) {
return fetchQuery<QueryTypes["response"]["settings"]>(
environment,
query,
{},
variables,
{ force: true }
);
}
@@ -26,6 +34,6 @@ export const withRefreshSettingsFetch = createFetchContainer(
fetch
);
export type RefreshSettingsFetch = () => Promise<
QueryTypes["response"]["settings"]
>;
export type RefreshSettingsFetch = (
variables: RefreshSettingsVariables
) => Promise<QueryTypes["response"]["settings"]>;
@@ -0,0 +1,27 @@
import { commitLocalUpdate, Environment } from "relay-runtime";
import { createMutationContainer } from "talk-framework/lib/relay";
export interface SetStoryClosedInput {
storyID: string;
isClosed: boolean;
}
export type SetStoryClosedMutation = (
input: SetStoryClosedInput
) => Promise<void>;
export async function commit(
environment: Environment,
input: SetStoryClosedInput
) {
return commitLocalUpdate(environment, store => {
const record = store.get(input.storyID)!;
record.setValue(input.isClosed, "isClosed");
});
}
export const withSetStoryClosedMutation = createMutationContainer(
"setStoryClosed",
commit
);
@@ -50,3 +50,8 @@ export {
withRemoveCommentReactionMutation,
RemoveCommentReactionMutation,
} from "./RemoveCommentReactionMutation";
export {
SetStoryClosedInput,
withSetStoryClosedMutation,
SetStoryClosedMutation,
} from "./SetStoryClosedMutation";
@@ -7,6 +7,7 @@ interface Props {
id?: string;
onClick?: EventHandler<MouseEvent<HTMLButtonElement>>;
active?: boolean;
disabled?: boolean;
}
const ReplyButton: StatelessComponent<Props> = props => (
@@ -16,6 +17,7 @@ const ReplyButton: StatelessComponent<Props> = props => (
variant="ghost"
size="small"
active={props.active}
disabled={props.disabled}
>
<MatchMedia gtWidth="xs">
<ButtonIcon>reply</ButtonIcon>
@@ -23,6 +23,8 @@ export interface PostCommentFormProps {
initialValues?: FormProps;
min: number | null;
max: number | null;
disabled?: boolean;
disabledMessage?: React.ReactNode;
}
const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
@@ -64,26 +66,38 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
}
value={input.value}
placeholder="Post a comment"
disabled={submitting}
disabled={submitting || props.disabled}
/>
</Localized>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
{props.disabled ? (
<>
{props.disabledMessage && (
<ValidationMessage fullWidth>
{props.disabledMessage}
</ValidationMessage>
)}
</>
) : (
<>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</>
)}
</HorizontalGutter>
<Flex direction="column" alignItems="flex-end">
@@ -91,7 +105,7 @@ const PostCommentForm: StatelessComponent<PostCommentFormProps> = props => (
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value}
disabled={submitting || !input.value || props.disabled}
type="submit"
>
Submit
@@ -0,0 +1,8 @@
.root {
}
.sitewide {
background-color: var(--palette-grey-dark);
border-color: var(--palette-grey-dark);
color: var(--palette-text-light);
}
@@ -0,0 +1,25 @@
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { PropTypesOf } from "talk-framework/types";
import PostCommentFormCollapsed from "./PostCommentFormCollapsed";
it("renders correctly when comments are closed sitewide", () => {
const props: PropTypesOf<typeof PostCommentFormCollapsed> = {
closedSitewide: true,
closedMessage: "closed site-wide",
};
const renderer = createRenderer();
renderer.render(<PostCommentFormCollapsed {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders correctly when story is closed", () => {
const props: PropTypesOf<typeof PostCommentFormCollapsed> = {
closedSitewide: false,
closedMessage: "closed story",
};
const renderer = createRenderer();
renderer.render(<PostCommentFormCollapsed {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -0,0 +1,21 @@
import cn from "classnames";
import React, { StatelessComponent } from "react";
import { CallOut } from "talk-ui/components";
import styles from "./PostCommentFormCollapsed.css";
interface Props {
closedSitewide?: boolean;
closedMessage?: React.ReactNode;
}
const PostCommentFormCollapsed: StatelessComponent<Props> = props => (
<CallOut
fullWidth
className={cn(styles.root, { [styles.sitewide]: props.closedSitewide })}
>
{props.closedMessage}
</CallOut>
);
export default PostCommentFormCollapsed;
@@ -39,6 +39,8 @@ export interface ReplyCommentFormProps {
parentUsername: string | null;
min: number | null;
max: number | null;
disabled?: boolean;
disabledMessage?: React.ReactNode;
}
const ReplyCommentForm: StatelessComponent<ReplyCommentFormProps> = props => {
@@ -84,27 +86,40 @@ const ReplyCommentForm: StatelessComponent<ReplyCommentFormProps> = props => {
value={input.value}
placeholder="Write a reply"
forwardRef={props.rteRef}
disabled={submitting}
disabled={submitting || props.disabled}
/>
</Localized>
</div>
{meta.touched &&
(meta.error ||
(meta.submitError && !meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
{props.disabled ? (
<>
{props.disabledMessage && (
<ValidationMessage fullWidth>
{props.disabledMessage}
</ValidationMessage>
)}
</>
) : (
<>
{meta.touched &&
(meta.error ||
(meta.submitError &&
!meta.dirtySinceLastSubmit)) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{submitError && (
<ValidationMessage fullWidth>
{submitError}
</ValidationMessage>
)}
{props.max && (
<RemainingCharactersContainer
value={input.value}
max={props.max}
/>
)}
</>
)}
</HorizontalGutter>
@@ -129,7 +144,9 @@ const ReplyCommentForm: StatelessComponent<ReplyCommentFormProps> = props => {
<Button
color="primary"
variant="filled"
disabled={submitting || !input.value}
disabled={
submitting || !input.value || props.disabled
}
type="submit"
fullWidth={matches}
>
@@ -10,7 +10,6 @@ import CommentContainer from "../containers/CommentContainer";
import CommunityGuidelinesContainer from "../containers/CommunityGuidelinesContainer";
import PostCommentFormContainer from "../containers/PostCommentFormContainer";
import ReplyListContainer from "../containers/ReplyListContainer";
import PostCommentFormFake from "./PostCommentFormFake";
import SortMenu from "./SortMenu";
import styles from "./Stream.css";
@@ -20,7 +19,8 @@ export interface StreamProps {
id: string;
isClosed?: boolean;
} & PropTypesOf<typeof CommentContainer>["story"] &
PropTypesOf<typeof ReplyListContainer>["story"];
PropTypesOf<typeof ReplyListContainer>["story"] &
PropTypesOf<typeof PostCommentFormContainer>["story"];
settings: PropTypesOf<typeof CommentContainer>["settings"] &
PropTypesOf<typeof ReplyListContainer>["settings"] &
PropTypesOf<typeof UserBoxContainer>["settings"] &
@@ -46,18 +46,9 @@ export interface StreamProps {
const Stream: StatelessComponent<StreamProps> = props => {
return (
<HorizontalGutter className={styles.root} size="double">
<HorizontalGutter>
<UserBoxContainer me={props.me} settings={props.settings} />
<CommunityGuidelinesContainer settings={props.settings} />
{props.me ? (
<PostCommentFormContainer
storyID={props.story.id}
settings={props.settings}
/>
) : (
<PostCommentFormFake />
)}
</HorizontalGutter>
<UserBoxContainer me={props.me} settings={props.settings} />
<CommunityGuidelinesContainer settings={props.settings} />
<PostCommentFormContainer settings={props.settings} story={props.story} />
{props.comments.length > 0 && (
<SortMenu orderBy={props.orderBy} onChange={props.onChangeOrderBy} />
)}
@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly when comments are closed sitewide 1`] = `
<withPropsOnChange(CallOut)
className="PostCommentFormCollapsed-root PostCommentFormCollapsed-sitewide"
fullWidth={true}
>
closed site-wide
</withPropsOnChange(CallOut)>
`;
exports[`renders correctly when story is closed 1`] = `
<withPropsOnChange(CallOut)
className="PostCommentFormCollapsed-root"
fullWidth={true}
>
closed story
</withPropsOnChange(CallOut)>
`;
@@ -5,30 +5,43 @@ exports[`renders correctly 1`] = `
className="Stream-root"
size="double"
>
<ForwardRef(forwardRef)>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
}
/>
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(withContext(withLocalStateContainer(Relay(PostCommentFormContainer))))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
story={
Object {
"id": "story-id",
"isClosed": false,
}
}
/>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
@@ -144,30 +157,43 @@ exports[`when there is more disables load more button 1`] = `
className="Stream-root"
size="double"
>
<ForwardRef(forwardRef)>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
}
/>
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(withContext(withLocalStateContainer(Relay(PostCommentFormContainer))))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
story={
Object {
"id": "story-id",
"isClosed": false,
}
}
/>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
@@ -297,30 +323,43 @@ exports[`when there is more renders a load more button 1`] = `
className="Stream-root"
size="double"
>
<ForwardRef(forwardRef)>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<PostCommentFormFake />
</ForwardRef(forwardRef)>
}
/>
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(withContext(withLocalStateContainer(Relay(PostCommentFormContainer))))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
story={
Object {
"id": "story-id",
"isClosed": false,
}
}
/>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
@@ -450,40 +489,43 @@ exports[`when use is logged in renders correctly 1`] = `
className="Stream-root"
size="double"
>
<ForwardRef(forwardRef)>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={Object {}}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(withContext(withLocalStateContainer(Relay(UserBoxContainer)))))))))
me={Object {}}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
/>
<Relay(CommunityGuidelinesContainerProps)
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
/>
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(Relay(PostCommentFormContainer))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
/>
<withContext(withContext(createMutationContainer(withContext(createFetchContainer(withContext(withLocalStateContainer(Relay(PostCommentFormContainer))))))))
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
storyID="story-id"
/>
</ForwardRef(forwardRef)>
}
story={
Object {
"id": "story-id",
"isClosed": false,
}
}
/>
<SortMenu
onChange={[Function]}
orderBy="CREATED_AT_ASC"
@@ -1,209 +1,128 @@
import { noop } from "lodash";
import { merge, noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import { DeepPartial, PropTypesOf } from "talk-framework/types";
import { CommentContainer } from "./CommentContainer";
// Remove relay refs so we can stub the props.
const CommentContainerN = removeFragmentRefs(CommentContainer);
it("renders username and body", () => {
const props: PropTypesOf<typeof CommentContainerN> = {
me: null,
story: {
url: "http://localhost/story",
},
comment: {
id: "comment-id",
author: {
id: "author-id",
username: "Marvin",
},
parent: null,
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
settings: {
reaction: {
icon: "thumb_up_alt",
label: "Respect",
},
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
localReply: false,
disableReplies: false,
};
type Props = PropTypesOf<typeof CommentContainerN>;
function createDefaultProps(add: DeepPartial<Props> = {}): Props {
return merge(
{},
{
me: null,
story: {
url: "http://localhost/story",
isClosed: false,
},
comment: {
id: "comment-id",
author: {
id: "author-id",
username: "Marvin",
},
parent: null,
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
settings: {
disableCommenting: {
enabled: false,
},
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
localReply: false,
disableReplies: false,
},
add
);
}
it("renders username and body", () => {
const props = createDefaultProps();
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders body only", () => {
const props: PropTypesOf<typeof CommentContainerN> = {
me: null,
story: {
url: "http://localhost/story",
},
const props = createDefaultProps({
comment: {
id: "comment-id",
author: {
id: "author-id",
username: null,
},
parent: null,
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
settings: {
reaction: {
icon: "thumb_up_alt",
label: "Respect",
},
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
};
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("hide reply button", () => {
const props: PropTypesOf<typeof CommentContainerN> = {
me: null,
story: {
url: "http://localhost/story",
},
comment: {
id: "comment-id",
author: {
id: "author-id",
username: "Marvin",
},
parent: null,
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
settings: {
reaction: {
icon: "thumb_up_alt",
label: "Respect",
},
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
localReply: false,
const props = createDefaultProps({
disableReplies: true,
};
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("shows conversation link", () => {
const props: PropTypesOf<typeof CommentContainerN> = {
me: null,
story: {
url: "http://localhost/story",
},
settings: {
reaction: {
icon: "thumb_up",
label: "Respect",
labelActive: "Respected",
},
},
comment: {
id: "comment-id",
author: {
id: "author-id",
username: "Marvin",
},
parent: null,
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
localReply: false,
disableReplies: false,
const props = createDefaultProps({
showConversationLink: true,
};
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders in reply to", () => {
const props: PropTypesOf<typeof CommentContainerN> = {
me: null,
story: {
url: "http://localhost/story",
},
const props = createDefaultProps({
comment: {
id: "comment-id",
author: {
id: "author-id",
username: "Marvin",
},
parent: {
author: {
username: "ParentAuthor",
},
},
body: "Woof",
createdAt: "1995-12-17T03:24:00.000Z",
editing: {
edited: false,
editableUntil: "1995-12-17T03:24:30.000Z",
},
pending: false,
},
settings: {
reaction: {
icon: "thumb_up_alt",
label: "Respect",
},
},
indentLevel: 1,
showAuthPopup: noop as any,
setCommentID: noop as any,
localReply: false,
disableReplies: false,
};
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders disabled reply when story is closed", () => {
const props = createDefaultProps({
story: {
isClosed: true,
},
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders disabled reply when commenting has been disabled", () => {
const props = createDefaultProps({
settings: {
disableCommenting: {
enabled: true,
},
},
});
const renderer = createRenderer();
renderer.render(<CommentContainerN {...props} />);
expect(renderer.getRenderOutput()).toMatchSnapshot();
@@ -153,6 +153,7 @@ export class CommentContainer extends Component<Props, State> {
<EditCommentFormContainer
settings={settings}
comment={comment}
story={story}
onClose={this.closeEditDialog}
/>
</div>
@@ -199,6 +200,9 @@ export class CommentContainer extends Component<Props, State> {
}`}
onClick={this.openReplyDialog}
active={showReplyDialog}
disabled={
settings.disableCommenting.enabled || story.isClosed
}
/>
)}
<PermalinkButtonContainer
@@ -258,8 +262,10 @@ const enhanced = withSetCommentIDMutation(
story: graphql`
fragment CommentContainer_story on Story {
url
isClosed
...ReplyCommentFormContainer_story
...PermalinkButtonContainer_story
...EditCommentFormContainer_story
}
`,
comment: graphql`
@@ -289,6 +295,9 @@ const enhanced = withSetCommentIDMutation(
`,
settings: graphql`
fragment CommentContainer_settings on Settings {
disableCommenting {
enabled
}
...ReactionButtonContainer_settings
...ReplyCommentFormContainer_settings
...EditCommentFormContainer_settings
@@ -14,6 +14,7 @@ import {
import { EditCommentFormContainer_comment as CommentData } from "talk-stream/__generated__/EditCommentFormContainer_comment.graphql";
import { EditCommentFormContainer_settings as SettingsData } from "talk-stream/__generated__/EditCommentFormContainer_settings.graphql";
import { EditCommentFormContainer_story as StoryData } from "talk-stream/__generated__/EditCommentFormContainer_story.graphql";
import {
EditCommentMutation,
withEditCommentMutation,
@@ -28,6 +29,7 @@ interface Props {
editComment: EditCommentMutation;
comment: CommentData;
settings: SettingsData;
story: StoryData;
onClose?: () => void;
autofocus: boolean;
refreshSettings: RefreshSettingsFetch;
@@ -92,7 +94,7 @@ export class EditCommentFormContainer extends Component<Props, State> {
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
await this.props.refreshSettings({ storyID: this.props.story.id });
}
return error.invalidArgs;
}
@@ -149,6 +151,11 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
}
}
`,
story: graphql`
fragment EditCommentFormContainer_story on Story {
id
}
`,
settings: graphql`
fragment EditCommentFormContainer_settings on Settings {
charCount {
@@ -1,39 +1,53 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import { merge, noop } from "lodash";
import React from "react";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import { DeepPartial, PropTypesOf } from "talk-framework/types";
import { PostCommentFormContainer } from "./PostCommentFormContainer";
const contextKey = "postCommentFormBody";
const PostCommentFormContainerN = removeFragmentRefs(PostCommentFormContainer);
function createDefaultProps(): PropTypesOf<typeof PostCommentFormContainerN> {
return {
createComment: noop as any,
refreshSettings: noop as any,
storyID: "story-id",
sessionStorage: createPromisifiedStorage(),
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
type Props = PropTypesOf<typeof PostCommentFormContainerN>;
function createDefaultProps(add: DeepPartial<Props> = {}): Props {
return merge(
{},
{
local: {
loggedIn: true,
},
createComment: noop as any,
refreshSettings: noop as any,
story: {
id: "story-id",
isClosed: false,
},
sessionStorage: createPromisifiedStorage(),
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
},
closedMessage: "closed",
disableCommenting: {
enabled: false,
message: "",
},
},
},
};
add
);
}
it("renders correctly", async () => {
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
@@ -41,9 +55,7 @@ it("renders correctly", async () => {
});
it("renders with initialValues", async () => {
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
await props.sessionStorage.setItem(contextKey, "Hello World!");
@@ -54,9 +66,7 @@ it("renders with initialValues", async () => {
});
it("save values", async () => {
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
await props.sessionStorage.setItem(contextKey, "Hello World!");
@@ -81,11 +91,13 @@ it("creates a comment", async () => {
.withArgs({})
.once();
const props: PropTypesOf<typeof PostCommentFormContainerN> = {
...createDefaultProps(),
const props = createDefaultProps({
createComment: createCommentStub,
storyID,
};
story: {
id: storyID,
isClosed: false,
},
});
await props.sessionStorage.setItem(contextKey, "Hello World!");
@@ -105,3 +117,79 @@ it("creates a comment", async () => {
await timeout();
formMock.verify();
});
it("renders when story has been closed (collapsing)", async () => {
const props = createDefaultProps({
story: {
isClosed: true,
},
settings: {
closedMessage: "story closed",
},
});
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("renders when commenting has been disabled (collapsing)", async () => {
const props = createDefaultProps({
settings: {
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
},
});
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
it("renders when story has been closed (non-collapsing)", async () => {
const props = createDefaultProps({
story: {
isClosed: false,
},
settings: {
closedMessage: "story closed",
},
});
const nextProps = createDefaultProps({
story: {
isClosed: true,
},
settings: {
closedMessage: "story closed",
},
});
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.setProps(nextProps);
expect(wrapper).toMatchSnapshot();
});
it("renders when commenting has been disabled (non-collapsing)", async () => {
const props = createDefaultProps({
settings: {
disableCommenting: {
enabled: false,
message: "commenting disabled",
},
},
});
const nextProps = createDefaultProps({
settings: {
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
},
});
const wrapper = shallow(<PostCommentFormContainerN {...props} />);
await timeout();
wrapper.setProps(nextProps);
expect(wrapper).toMatchSnapshot();
});
@@ -2,10 +2,16 @@ import React, { Component } from "react";
import { withContext } from "talk-framework/lib/bootstrap";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
import {
graphql,
withFragmentContainer,
withLocalStateContainer,
} from "talk-framework/lib/relay";
import { PromisifiedStorage } from "talk-framework/lib/storage";
import { PropTypesOf } from "talk-framework/types";
import { PostCommentFormContainer_settings as SettingsData } from "talk-stream/__generated__/PostCommentFormContainer_settings.graphql";
import { PostCommentFormContainer_story as StoryData } from "talk-stream/__generated__/PostCommentFormContainer_story.graphql";
import { PostCommentFormContainerLocal as Local } from "talk-stream/__generated__/PostCommentFormContainerLocal.graphql";
import {
RefreshSettingsFetch,
withRefreshSettingsFetch,
@@ -18,25 +24,35 @@ import {
import PostCommentForm, {
PostCommentFormProps,
} from "../components/PostCommentForm";
import PostCommentFormCollapsed from "../components/PostCommentFormCollapsed";
import PostCommentFormFake from "../components/PostCommentFormFake";
import { shouldTriggerSettingsRefresh } from "../helpers";
interface Props {
createComment: CreateCommentMutation;
refreshSettings: RefreshSettingsFetch;
storyID: string;
sessionStorage: PromisifiedStorage;
settings: SettingsData;
local: Local;
story: StoryData;
}
interface State {
initialValues?: PostCommentFormProps["initialValues"];
initialized: boolean;
keepFormWhenClosed: boolean;
}
const contextKey = "postCommentFormBody";
export class PostCommentFormContainer extends Component<Props, State> {
public state: State = { initialized: false };
public state: State = {
initialized: false,
keepFormWhenClosed:
this.props.local.loggedIn &&
!this.props.story.isClosed &&
!this.props.settings.disableCommenting.enabled,
};
constructor(props: Props) {
super(props);
@@ -63,14 +79,14 @@ export class PostCommentFormContainer extends Component<Props, State> {
) => {
try {
await this.props.createComment({
storyID: this.props.storyID,
storyID: this.props.story.id,
...input,
});
form.reset({});
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
await this.props.refreshSettings({ storyID: this.props.story.id });
}
return error.invalidArgs;
}
@@ -96,6 +112,25 @@ export class PostCommentFormContainer extends Component<Props, State> {
if (!this.state.initialized) {
return null;
}
if (
!this.state.keepFormWhenClosed &&
(this.props.settings.disableCommenting.enabled ||
this.props.story.isClosed)
) {
return (
<PostCommentFormCollapsed
closedSitewide={this.props.settings.disableCommenting.enabled}
closedMessage={
(this.props.settings.disableCommenting.enabled &&
this.props.settings.disableCommenting.message) ||
this.props.settings.closedMessage
}
/>
);
}
if (!this.props.local.loggedIn) {
return <PostCommentFormFake />;
}
return (
<PostCommentForm
onSubmit={this.handleOnSubmit}
@@ -111,6 +146,15 @@ export class PostCommentFormContainer extends Component<Props, State> {
this.props.settings.charCount.max) ||
null
}
disabled={
this.props.settings.disableCommenting.enabled ||
this.props.story.isClosed
}
disabledMessage={
(this.props.settings.disableCommenting.enabled &&
this.props.settings.disableCommenting.message) ||
this.props.settings.closedMessage
}
/>
);
}
@@ -121,17 +165,36 @@ const enhanced = withContext(({ sessionStorage }) => ({
}))(
withCreateCommentMutation(
withRefreshSettingsFetch(
withFragmentContainer<Props>({
settings: graphql`
fragment PostCommentFormContainer_settings on Settings {
charCount {
enabled
min
max
}
withLocalStateContainer(
graphql`
fragment PostCommentFormContainerLocal on Local {
loggedIn
}
`,
})(PostCommentFormContainer)
`
)(
withFragmentContainer<Props>({
settings: graphql`
fragment PostCommentFormContainer_settings on Settings {
charCount {
enabled
min
max
}
disableCommenting {
enabled
message
}
closedMessage
}
`,
story: graphql`
fragment PostCommentFormContainer_story on Story {
id
isClosed
}
`,
})(PostCommentFormContainer)
)
)
)
);
@@ -1,5 +1,5 @@
import { shallow } from "enzyme";
import { noop } from "lodash";
import { merge, noop } from "lodash";
import React from "react";
import { createRenderer } from "react-test-renderer/shallow";
import sinon from "sinon";
@@ -7,7 +7,7 @@ import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { removeFragmentRefs } from "talk-framework/testHelpers";
import { PropTypesOf } from "talk-framework/types";
import { DeepPartial, PropTypesOf } from "talk-framework/types";
import { ReplyCommentFormContainer } from "./ReplyCommentFormContainer";
const ReplyCommentFormContainerN = removeFragmentRefs(
@@ -18,38 +18,48 @@ function getContextKey(commentID: string) {
return `replyCommentFormBody-${commentID}`;
}
function createDefaultProps(): PropTypesOf<typeof ReplyCommentFormContainerN> {
return {
createCommentReply: noop as any,
refreshSettings: noop as any,
story: {
id: "story-id",
},
comment: {
id: "comment-id",
author: {
username: "Joe",
type Props = PropTypesOf<typeof ReplyCommentFormContainerN>;
function createDefaultProps(add: DeepPartial<Props> = {}): Props {
return merge(
{},
{
createCommentReply: noop as any,
refreshSettings: noop as any,
story: {
id: "story-id",
isClosed: false,
},
revision: {
id: "revision-id",
comment: {
id: "comment-id",
author: {
username: "Joe",
},
revision: {
id: "revision-id",
},
},
sessionStorage: createPromisifiedStorage(),
autofocus: false,
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
},
closedMessage: "closed",
disableCommenting: {
enabled: false,
message: "",
},
},
},
sessionStorage: createPromisifiedStorage(),
autofocus: false,
settings: {
charCount: {
enabled: true,
min: 3,
max: 100,
},
},
};
add
);
}
it("renders correctly", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
const renderer = createRenderer();
renderer.render(<ReplyCommentFormContainerN {...props} />);
@@ -58,9 +68,7 @@ it("renders correctly", async () => {
});
it("renders with initialValues", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
@@ -74,9 +82,7 @@ it("renders with initialValues", async () => {
});
it("save values", async () => {
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
};
const props = createDefaultProps();
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
@@ -102,11 +108,10 @@ it("creates a comment", async () => {
const form = { reset: noop };
const onCloseStub = sinon.stub();
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
const props = createDefaultProps({
onClose: onCloseStub,
createCommentReply: createCommentStub,
};
});
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
@@ -134,10 +139,9 @@ it("creates a comment", async () => {
it("closes on cancel", async () => {
const onCloseStub = sinon.stub();
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
const props = createDefaultProps({
onClose: onCloseStub,
};
});
await props.sessionStorage.setItem(
getContextKey(props.comment.id),
@@ -161,10 +165,10 @@ it("closes on cancel", async () => {
it("autofocuses", async () => {
const focusStub = sinon.stub();
const rte = { focus: focusStub };
const props: PropTypesOf<typeof ReplyCommentFormContainerN> = {
...createDefaultProps(),
const props = createDefaultProps({
autofocus: true,
};
});
const wrapper = shallow(<ReplyCommentFormContainerN {...props} />);
await timeout();
@@ -175,3 +179,34 @@ it("autofocuses", async () => {
.rteRef(rte);
expect(focusStub.calledOnce).toBe(true);
});
it("renders when story has been closed", async () => {
const props = createDefaultProps({
story: {
isClosed: true,
},
settings: {
closedMessage: "story closed",
},
});
const renderer = createRenderer();
renderer.render(<ReplyCommentFormContainerN {...props} />);
await timeout();
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
it("renders when commenting has been disabled", async () => {
const props = createDefaultProps({
settings: {
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
},
});
const renderer = createRenderer();
renderer.render(<ReplyCommentFormContainerN {...props} />);
await timeout();
expect(renderer.getRenderOutput()).toMatchSnapshot();
});
@@ -97,7 +97,7 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
} catch (error) {
if (error instanceof InvalidRequestError) {
if (shouldTriggerSettingsRefresh(error.code)) {
await this.props.refreshSettings();
await this.props.refreshSettings({ storyID: this.props.story.id });
}
return error.invalidArgs;
}
@@ -144,6 +144,15 @@ export class ReplyCommentFormContainer extends Component<Props, State> {
this.props.settings.charCount.max) ||
null
}
disabled={
this.props.settings.disableCommenting.enabled ||
this.props.story.isClosed
}
disabledMessage={
(this.props.settings.disableCommenting.enabled &&
this.props.settings.disableCommenting.message) ||
this.props.settings.closedMessage
}
/>
);
}
@@ -163,11 +172,17 @@ const enhanced = withContext(({ sessionStorage, browserInfo }) => ({
min
max
}
disableCommenting {
enabled
message
}
closedMessage
}
`,
story: graphql`
fragment ReplyCommentFormContainer_story on Story {
id
isClosed
}
`,
comment: graphql`
@@ -10,6 +10,7 @@ import {
COMMENT_SORT,
StreamContainerPaginationQueryVariables,
} from "talk-stream/__generated__/StreamContainerPaginationQuery.graphql";
import StoryClosedTimeoutContainer from "talk-stream/containers/StoryClosedTimeoutContainer";
import Stream from "../components/Stream";
@@ -59,18 +60,21 @@ export class StreamContainer extends React.Component<Props> {
public render() {
const comments = this.props.story.comments.edges.map(edge => edge.node);
return (
<Stream
story={this.props.story}
comments={comments}
settings={this.props.settings}
onLoadMore={this.loadMore}
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
me={this.props.me}
orderBy={this.orderBy}
onChangeOrderBy={this.handleOnChangeOrderBy}
refetching={this.state.refetching}
/>
<>
<StoryClosedTimeoutContainer story={this.props.story} />
<Stream
story={this.props.story}
comments={comments}
settings={this.props.settings}
onLoadMore={this.loadMore}
hasMore={this.props.relay.hasMore()}
disableLoadMore={this.state.disableLoadMore}
me={this.props.me}
orderBy={this.orderBy}
onChangeOrderBy={this.handleOnChangeOrderBy}
refetching={this.state.refetching}
/>
</>
);
}
@@ -122,8 +126,10 @@ const enhanced = withPaginationContainer<
}
}
}
...PostCommentFormContainer_story
...CommentContainer_story
...ReplyListContainer1_story
...StoryClosedTimeoutContainer_story
}
`,
me: graphql`
@@ -19,6 +19,7 @@ exports[`hide reply button 1`] = `
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
@@ -44,9 +45,8 @@ exports[`hide reply button 1`] = `
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
"disableCommenting": Object {
"enabled": false,
},
}
}
@@ -103,6 +103,7 @@ exports[`renders body only 1`] = `
<ButtonsBar>
<ReplyButton
active={false}
disabled={false}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
@@ -110,6 +111,7 @@ exports[`renders body only 1`] = `
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
@@ -135,9 +137,8 @@ exports[`renders body only 1`] = `
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
"disableCommenting": Object {
"enabled": false,
},
}
}
@@ -177,6 +178,190 @@ exports[`renders body only 1`] = `
</div>
`;
exports[`renders disabled reply when commenting has been disabled 1`] = `
<div
data-testid="comment-comment-id"
>
<ForwardRef(forwardRef)>
<IndentedComment
blur={false}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
footer={
<React.Fragment>
<ForwardRef(forwardRef)
justifyContent="space-between"
>
<ButtonsBar>
<ReplyButton
active={false}
disabled={true}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
/>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(Relay(ReactionButtonContainer)))))))
comment={
Object {
"author": Object {
"id": "author-id",
"username": "Marvin",
},
"body": "Woof",
"createdAt": "1995-12-17T03:24:00.000Z",
"editing": Object {
"editableUntil": "1995-12-17T03:24:30.000Z",
"edited": false,
},
"id": "comment-id",
"parent": null,
"pending": false,
}
}
me={null}
settings={
Object {
"disableCommenting": Object {
"enabled": true,
},
}
}
/>
</ButtonsBar>
<ButtonsBar>
<withContext(createMutationContainer(Relay(ReportButtonContainer)))
comment={
Object {
"author": Object {
"id": "author-id",
"username": "Marvin",
},
"body": "Woof",
"createdAt": "1995-12-17T03:24:00.000Z",
"editing": Object {
"editableUntil": "1995-12-17T03:24:30.000Z",
"edited": false,
},
"id": "comment-id",
"parent": null,
"pending": false,
}
}
me={null}
/>
</ButtonsBar>
</ForwardRef(forwardRef)>
</React.Fragment>
}
indentLevel={1}
parentAuthorName={null}
showEditedMarker={false}
username="Marvin"
/>
</ForwardRef(forwardRef)>
</div>
`;
exports[`renders disabled reply when story is closed 1`] = `
<div
data-testid="comment-comment-id"
>
<ForwardRef(forwardRef)>
<IndentedComment
blur={false}
body="Woof"
createdAt="1995-12-17T03:24:00.000Z"
footer={
<React.Fragment>
<ForwardRef(forwardRef)
justifyContent="space-between"
>
<ButtonsBar>
<ReplyButton
active={false}
disabled={true}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
<Relay(PermalinkButtonContainerProps)
commentID="comment-id"
story={
Object {
"isClosed": true,
"url": "http://localhost/story",
}
}
/>
<withContext(createMutationContainer(withContext(createMutationContainer(withContext(createMutationContainer(Relay(ReactionButtonContainer)))))))
comment={
Object {
"author": Object {
"id": "author-id",
"username": "Marvin",
},
"body": "Woof",
"createdAt": "1995-12-17T03:24:00.000Z",
"editing": Object {
"editableUntil": "1995-12-17T03:24:30.000Z",
"edited": false,
},
"id": "comment-id",
"parent": null,
"pending": false,
}
}
me={null}
settings={
Object {
"disableCommenting": Object {
"enabled": false,
},
}
}
/>
</ButtonsBar>
<ButtonsBar>
<withContext(createMutationContainer(Relay(ReportButtonContainer)))
comment={
Object {
"author": Object {
"id": "author-id",
"username": "Marvin",
},
"body": "Woof",
"createdAt": "1995-12-17T03:24:00.000Z",
"editing": Object {
"editableUntil": "1995-12-17T03:24:30.000Z",
"edited": false,
},
"id": "comment-id",
"parent": null,
"pending": false,
}
}
me={null}
/>
</ButtonsBar>
</ForwardRef(forwardRef)>
</React.Fragment>
}
indentLevel={1}
parentAuthorName={null}
showEditedMarker={false}
username="Marvin"
/>
</ForwardRef(forwardRef)>
</div>
`;
exports[`renders in reply to 1`] = `
<div
data-testid="comment-comment-id"
@@ -194,6 +379,7 @@ exports[`renders in reply to 1`] = `
<ButtonsBar>
<ReplyButton
active={false}
disabled={false}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
@@ -201,6 +387,7 @@ exports[`renders in reply to 1`] = `
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
@@ -230,9 +417,8 @@ exports[`renders in reply to 1`] = `
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
"disableCommenting": Object {
"enabled": false,
},
}
}
@@ -293,6 +479,7 @@ exports[`renders username and body 1`] = `
<ButtonsBar>
<ReplyButton
active={false}
disabled={false}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
@@ -300,6 +487,7 @@ exports[`renders username and body 1`] = `
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
@@ -325,9 +513,8 @@ exports[`renders username and body 1`] = `
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
"disableCommenting": Object {
"enabled": false,
},
}
}
@@ -384,6 +571,7 @@ exports[`shows conversation link 1`] = `
<ButtonsBar>
<ReplyButton
active={false}
disabled={false}
id="comments-commentContainer-replyButton-comment-id"
onClick={[Function]}
/>
@@ -391,6 +579,7 @@ exports[`shows conversation link 1`] = `
commentID="comment-id"
story={
Object {
"isClosed": false,
"url": "http://localhost/story",
}
}
@@ -416,10 +605,8 @@ exports[`shows conversation link 1`] = `
me={null}
settings={
Object {
"reaction": Object {
"icon": "thumb_up",
"label": "Respect",
"labelActive": "Respected",
"disableCommenting": Object {
"enabled": false,
},
}
}
@@ -2,6 +2,44 @@
exports[`renders correctly 1`] = `
<PostCommentForm
disabled={false}
disabledMessage="closed"
max={100}
min={3}
onChange={[Function]}
onSubmit={[Function]}
/>
`;
exports[`renders when commenting has been disabled (collapsing) 1`] = `
<PostCommentFormCollapsed
closedMessage="commenting disabled"
closedSitewide={true}
/>
`;
exports[`renders when commenting has been disabled (non-collapsing) 1`] = `
<PostCommentForm
disabled={true}
disabledMessage="commenting disabled"
max={100}
min={3}
onChange={[Function]}
onSubmit={[Function]}
/>
`;
exports[`renders when story has been closed (collapsing) 1`] = `
<PostCommentFormCollapsed
closedMessage="story closed"
closedSitewide={false}
/>
`;
exports[`renders when story has been closed (non-collapsing) 1`] = `
<PostCommentForm
disabled={true}
disabledMessage="story closed"
max={100}
min={3}
onChange={[Function]}
@@ -11,6 +49,8 @@ exports[`renders correctly 1`] = `
exports[`renders with initialValues 1`] = `
<PostCommentForm
disabled={false}
disabledMessage="closed"
initialValues={
Object {
"body": "Hello World!",
@@ -2,6 +2,38 @@
exports[`renders correctly 1`] = `
<ReplyCommentForm
disabled={false}
disabledMessage="closed"
id="comment-id"
max={100}
min={3}
onCancel={[Function]}
onChange={[Function]}
onSubmit={[Function]}
parentUsername="Joe"
rteRef={[Function]}
/>
`;
exports[`renders when commenting has been disabled 1`] = `
<ReplyCommentForm
disabled={true}
disabledMessage="commenting disabled"
id="comment-id"
max={100}
min={3}
onCancel={[Function]}
onChange={[Function]}
onSubmit={[Function]}
parentUsername="Joe"
rteRef={[Function]}
/>
`;
exports[`renders when story has been closed 1`] = `
<ReplyCommentForm
disabled={true}
disabledMessage="story closed"
id="comment-id"
max={100}
min={3}
@@ -15,6 +47,8 @@ exports[`renders correctly 1`] = `
exports[`renders with initialValues 1`] = `
<ReplyCommentForm
disabled={false}
disabledMessage="closed"
id="comment-id"
initialValues={
Object {
@@ -1,200 +1,296 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Stream
comments={
Array [
<Fragment>
<withContext(createMutationContainer(Relay(StoryClosedTimeoutContainer)))
story={
Object {
"id": "comment-1",
},
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
Object {
"node": Object {
"id": "comment-2",
},
},
],
},
"id": "story-id",
"isClosed": false,
}
}
/>
<Stream
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
}
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
},
Object {
"node": Object {
"id": "comment-2",
Object {
"node": Object {
"id": "comment-2",
},
},
},
],
},
"id": "story-id",
"isClosed": false,
],
},
"id": "story-id",
"isClosed": false,
}
}
}
/>
/>
</Fragment>
`;
exports[`when has more comments renders hasMore 1`] = `
<Stream
comments={
Array [
<Fragment>
<withContext(createMutationContainer(Relay(StoryClosedTimeoutContainer)))
story={
Object {
"id": "comment-1",
},
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
Object {
"node": Object {
"id": "comment-2",
},
},
],
},
"id": "story-id",
"isClosed": false,
}
}
/>
<Stream
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
}
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
},
Object {
"node": Object {
"id": "comment-2",
Object {
"node": Object {
"id": "comment-2",
},
},
},
],
},
"id": "story-id",
"isClosed": false,
],
},
"id": "story-id",
"isClosed": false,
}
}
}
/>
/>
</Fragment>
`;
exports[`when has more comments when loading more disables load more button 1`] = `
<Stream
comments={
Array [
<Fragment>
<withContext(createMutationContainer(Relay(StoryClosedTimeoutContainer)))
story={
Object {
"id": "comment-1",
},
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
Object {
"node": Object {
"id": "comment-2",
},
},
],
},
"id": "story-id",
"isClosed": false,
}
}
/>
<Stream
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={true}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"id": "comment-2",
},
]
}
disableLoadMore={true}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
}
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
},
Object {
"node": Object {
"id": "comment-2",
Object {
"node": Object {
"id": "comment-2",
},
},
},
],
},
"id": "story-id",
"isClosed": false,
],
},
"id": "story-id",
"isClosed": false,
}
}
}
/>
/>
</Fragment>
`;
exports[`when has more comments when loading more enable load more button after loading is done 1`] = `
<Stream
comments={
Array [
<Fragment>
<withContext(createMutationContainer(Relay(StoryClosedTimeoutContainer)))
story={
Object {
"id": "comment-1",
},
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
Object {
"node": Object {
"id": "comment-2",
},
},
],
},
"id": "story-id",
"isClosed": false,
}
}
/>
<Stream
comments={
Array [
Object {
"id": "comment-1",
},
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"id": "comment-2",
},
]
}
disableLoadMore={false}
hasMore={true}
me={null}
onChangeOrderBy={[Function]}
onLoadMore={[Function]}
orderBy="CREATED_AT_ASC"
refetching={false}
settings={
Object {
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
"reaction": Object {
"icon": "thumb_up_alt",
"label": "Respect",
},
}
}
}
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
story={
Object {
"comments": Object {
"edges": Array [
Object {
"node": Object {
"id": "comment-1",
},
},
},
Object {
"node": Object {
"id": "comment-2",
Object {
"node": Object {
"id": "comment-2",
},
},
},
],
},
"id": "story-id",
"isClosed": false,
],
},
"id": "story-id",
"isClosed": false,
}
}
}
/>
/>
</Fragment>
`;
@@ -3,6 +3,8 @@ import { ERROR_CODES } from "talk-common/errors";
const triggers = [
ERROR_CODES.COMMENT_BODY_TOO_SHORT,
ERROR_CODES.COMMENT_BODY_EXCEEDS_MAX_LENGTH,
ERROR_CODES.COMMENTING_DISABLED,
ERROR_CODES.STORY_CLOSED,
];
/**
* shouldTriggerSettingsRefresh will indicate whether the settings
@@ -74,6 +74,7 @@ exports[`cancel edit 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -752,6 +753,7 @@ exports[`edit a comment: render comment with edit button 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -941,6 +943,7 @@ exports[`edit a comment: server response 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -1121,6 +1124,7 @@ exports[`shows expiry message: edit form closed 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -70,6 +70,7 @@ exports[`renders comment stream with load more button 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -219,6 +220,7 @@ exports[`renders comment stream with load more button 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -157,6 +157,7 @@ exports[`renders permalink view 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -323,6 +324,7 @@ exports[`renders permalink view 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-2"
onBlur={[Function]}
onClick={[Function]}
@@ -487,6 +489,7 @@ exports[`renders permalink view 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
@@ -647,6 +650,7 @@ exports[`renders permalink view 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-3"
onBlur={[Function]}
onClick={[Function]}
@@ -796,6 +800,7 @@ exports[`renders permalink view 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-4"
onBlur={[Function]}
onClick={[Function]}
@@ -180,6 +180,7 @@ exports[`renders conversation thread 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -359,6 +360,7 @@ exports[`shows more of this conversation 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -525,6 +527,7 @@ exports[`shows more of this conversation 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-2"
onBlur={[Function]}
onClick={[Function]}
@@ -689,6 +692,7 @@ exports[`shows more of this conversation 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -74,6 +74,7 @@ exports[`post a comment: optimistic response 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-uuid-0"
onBlur={[Function]}
onClick={[Function]}
@@ -60,6 +60,7 @@ exports[`post a reply: open reply form 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost Button-active"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-5"
onBlur={[Function]}
onClick={[Function]}
@@ -579,6 +580,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies"
onBlur={[Function]}
onClick={[Function]}
@@ -751,6 +753,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-1"
onBlur={[Function]}
onClick={[Function]}
@@ -923,6 +926,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-2"
onBlur={[Function]}
onClick={[Function]}
@@ -1095,6 +1099,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-3"
onBlur={[Function]}
onClick={[Function]}
@@ -1267,6 +1272,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-4"
onBlur={[Function]}
onClick={[Function]}
@@ -1439,6 +1445,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-5"
onBlur={[Function]}
onClick={[Function]}
@@ -1,323 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`post a reply and handle server error: open reply form 1`] = `
<div
data-testid="comment-comment-0"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="Indent-root"
>
<div
className=""
>
<div
className="Comment-root"
role="article"
>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween Flex-directionRow"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-alignBaseline Flex-directionColumn"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary Username-root"
>
Markus
</span>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignBaseline Flex-directionRow"
>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
<div
className="Flex-root Flex-flex Flex-justifySpaceBetween"
>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost Button-active"
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Reply
</span>
</button>
<div
className="Popover-root"
>
<button
aria-controls="permalink-popover-comment-0"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Share
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="permalink-popover-comment-0-ariainfo"
id="permalink-popover-comment-0"
role="popup"
>
<div
className="AriaInfo-root"
id="permalink-popover-comment-0-ariainfo"
>
A dialog showing a permalink to the comment
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Respect
</span>
</button>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-directionRow"
>
<div
className="Popover-root"
>
<button
aria-controls="report-popover-comment-0"
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Report
</span>
</button>
<div
aria-hidden={true}
aria-labelledby="report-popover-comment-0-ariainfo"
id="report-popover-comment-0"
role="popup"
>
<div
className="AriaInfo-root"
id="report-popover-comment-0-ariainfo"
>
A dialog for reporting comments
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<form
autoComplete="off"
id="comments-replyCommentForm-form-comment-0"
onSubmit={[Function]}
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<div
className="HorizontalGutter-root HorizontalGutter-half"
>
<div>
<label
className="AriaInfo-root"
htmlFor="comments-replyCommentForm-rte-comment-0"
>
Write a reply
</label>
<div
className="Flex-root ReplyTo-root Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
reply
</span>
<span>
 
</span>
<span
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary ReplyTo-text"
>
Replying to:
<span
className="Typography-root Typography-heading4 Typography-colorTextPrimary ReplyTo-username"
>
Markus
</span>
</span>
</div>
<div>
<div
className=""
>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder"
>
Write a reply
</div>
<div
aria-placeholder="Write a reply"
className="RTE-contentEditable RTE-content"
contentEditable={true}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
}
disabled={false}
id="comments-replyCommentForm-rte-comment-0"
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="Flex-root Flex-flex Flex-halfItemGutter Flex-justifyFlexEnd Flex-directionRow"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Cancel
</button>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Submit
</button>
</div>
</div>
</form>
</div>
</div>
`;
exports[`post a reply: open reply form 1`] = `
<div
data-testid="comment-comment-0"
@@ -378,6 +60,7 @@ exports[`post a reply: open reply form 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost Button-active"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -719,6 +402,7 @@ exports[`post a reply: optimistic response 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-uuid-0"
onBlur={[Function]}
onClick={[Function]}
@@ -12,146 +12,142 @@ exports[`renders comment stream with community guidelines 1`] = `
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div
className="CallOut-root CallOut-colorPrimary"
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div
className="CallOut-root CallOut-colorPrimary"
>
<div
className="Markdown-root"
dangerouslySetInnerHTML={
Object {
"__html": "<h2 id=\\"community-guidelines\\">Community Guidelines</h2>
className="Markdown-root"
dangerouslySetInnerHTML={
Object {
"__html": "<h2 id=\\"community-guidelines\\">Community Guidelines</h2>
",
}
}
/>
</div>
}
/>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
aria-hidden="true"
>
<div
aria-hidden="true"
>
<div>
<div>
<div
className=""
>
<div
className=""
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
format_quote
</span>
</button>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<div>
<div
@@ -281,6 +277,7 @@ exports[`renders comment stream with community guidelines 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -430,6 +427,7 @@ exports[`renders comment stream with community guidelines 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -69,6 +69,7 @@ exports[`renders reply list 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-replies"
onBlur={[Function]}
onClick={[Function]}
@@ -223,6 +224,7 @@ exports[`renders reply list 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-3"
onBlur={[Function]}
onClick={[Function]}
@@ -372,6 +374,7 @@ exports[`renders reply list 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-4"
onBlur={[Function]}
onClick={[Function]}
@@ -523,6 +526,7 @@ exports[`renders reply list 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-5"
onBlur={[Function]}
onClick={[Function]}
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders app with comment stream 1`] = `
exports[`renders comment stream 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
@@ -43,133 +43,129 @@ exports[`renders app with comment stream 1`] = `
className="HorizontalGutter-root Stream-root HorizontalGutter-double"
>
<div
className="HorizontalGutter-root HorizontalGutter-full"
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter"
aria-hidden="true"
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign in
</button>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantOutlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Register
</button>
</div>
<div
className="HorizontalGutter-root PostCommentFormFake-root HorizontalGutter-full"
>
<div
aria-hidden="true"
>
<div>
<div>
<div
className=""
>
<div
className=""
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
<div
aria-hidden="true"
className="RTE-placeholder RTE-placeholder "
>
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
Post a comment
</div>
<div
aria-placeholder="Post a comment"
className="RTE-contentEditable RTE-content RTE-contentEditableDisabled"
contentEditable={false}
dangerouslySetInnerHTML={
Object {
"__html": "",
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
}
disabled={true}
onBlur={[Function]}
onChange={[Function]}
onCut={[Function]}
onFocus={[Function]}
onInput={[Function]}
onKeyDown={[Function]}
onPaste={[Function]}
onSelect={[Function]}
/>
<div
className="RTE-toolbar RTE-toolbarDisabled RTE-toolbarBottom Toolbar-toolbar"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Bold"
type="button"
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
format_bold
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Italic"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
format_italic
</span>
</button>
<button
className="Button-button"
disabled={false}
onClick={[Function]}
title="Blockquote"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
<span
aria-hidden="true"
className="Icon-root Icon-md"
>
format_quote
</span>
</button>
</div>
format_quote
</span>
</button>
</div>
</div>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-fullWidth Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Sign in and Join the Conversation
</button>
</div>
<div>
<div
@@ -299,6 +295,7 @@ exports[`renders app with comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-0"
onBlur={[Function]}
onClick={[Function]}
@@ -448,6 +445,7 @@ exports[`renders app with comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -69,6 +69,7 @@ exports[`renders comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-1"
onBlur={[Function]}
onClick={[Function]}
@@ -60,6 +60,7 @@ exports[`renders deepest comment with link 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-with-deepest-replies-5"
onBlur={[Function]}
onClick={[Function]}
@@ -70,6 +70,7 @@ exports[`renders app with comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-2"
onBlur={[Function]}
onClick={[Function]}
@@ -219,6 +220,7 @@ exports[`renders app with comment stream 1`] = `
>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Button-variantGhost"
disabled={false}
id="comments-commentContainer-replyButton-comment-3"
onBlur={[Function]}
onClick={[Function]}
@@ -0,0 +1,99 @@
import sinon from "sinon";
import { waitForElement, within } from "talk-framework/testHelpers";
import { settings, stories } from "../fixtures";
import create from "./create";
afterEach(() => jest.useRealTimers());
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
...resolver,
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
},
});
return {
testRenderer,
context,
};
}
it("renders disabled comment stream", async () => {
const { testRenderer } = await createTestRenderer({
Query: {
settings: sinon.stub().callsFake(() => ({
...settings,
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
})),
},
});
await waitForElement(() =>
within(testRenderer.root).getByText("commenting disabled")
);
});
it("renders closed comment stream", async () => {
const { testRenderer } = await createTestRenderer({
Query: {
story: sinon.stub().callsFake(() => ({
...stories[0],
isClosed: true,
})),
},
});
await waitForElement(() =>
within(testRenderer.root).getByText("Story is closed")
);
});
it("auto close comment stream when story closed at has been reached", async () => {
const closeIn = 360000;
const now = new Date();
const later = new Date(now.valueOf() + closeIn);
jest.useFakeTimers();
const { testRenderer } = await createTestRenderer({
Query: {
story: sinon.stub().callsFake(() => ({
...stories[0],
closedAt: later.toISOString(),
isClosed: false,
})),
},
});
expect(within(testRenderer.root).queryByText("Story is closed")).toBeNull();
await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
jest.advanceTimersByTime(closeIn);
await waitForElement(() =>
within(testRenderer.root).getByText("Story is closed")
);
});
@@ -5,14 +5,16 @@ import { ERROR_CODES } from "talk-common/errors";
import { InvalidRequestError } from "talk-framework/lib/errors";
import {
createSinonStub,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import RTE from "@coralproject/rte";
import { baseComment, settings, stories, users } from "../fixtures";
import create from "./create";
function createTestRenderer(
async function createTestRenderer(
resolver: any,
options: { muteNetworkErrors?: boolean } = {}
) {
@@ -21,17 +23,15 @@ function createTestRenderer(
Query: {
settings: sinon.stub().returns(settings),
me: sinon.stub().returns(users[0]),
story: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: stories[0].id, url: null })
.returns(stories[0])
),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
},
};
return create({
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
@@ -41,10 +41,34 @@ function createTestRenderer(
localRecord.setValue(true, "loggedIn");
},
});
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
const rte = await waitForElement(
() =>
findParentWithType(
within(tabPane).getByLabelText("Post a comment"),
// 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,
tabPane,
rte,
form,
};
}
it("post a comment", async () => {
const { testRenderer } = createTestRenderer({
const { rte, form, tabPane } = await createTestRenderer({
Mutation: {
createComment: sinon.stub().callsFake((_, data) => {
expect(data).toEqual({
@@ -70,20 +94,10 @@ it("post a comment", async () => {
},
});
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
testRenderer.root
.findByProps({ inputId: "comments-postCommentForm-field" })
.props.onChange({ html: "<b>Hello world!</b>" });
rte.props.onChange({ html: "<b>Hello world!</b>" });
timekeeper.freeze(new Date(baseComment.createdAt));
testRenderer.root
.findByProps({ id: "comments-postCommentForm-form" })
.props.onSubmit();
form.props.onSubmit();
timekeeper.reset();
// Test optimistic response.
@@ -100,7 +114,7 @@ it("post a comment", async () => {
});
it("post a comment and handle server error", async () => {
const { testRenderer } = createTestRenderer(
const { form, rte, tabPane } = await createTestRenderer(
{
Mutation: {
createComment: sinon.stub().callsFake(() => {
@@ -111,22 +125,74 @@ it("post a comment and handle server error", async () => {
{ muteNetworkErrors: true }
);
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
testRenderer.root
.findByProps({ inputId: "comments-postCommentForm-field" })
.props.onChange({ html: "<b>Hello world!</b>" });
rte.props.onChange({ html: "<b>Hello world!</b>" });
timekeeper.freeze(new Date(baseComment.createdAt));
testRenderer.root
.findByProps({ id: "comments-postCommentForm-form" })
.props.onSubmit();
form.props.onSubmit();
timekeeper.reset();
// Look for internal error being displayed.
await waitForElement(() => within(tabPane).getByText("INTERNAL_ERROR"));
});
it("handle disabled commenting error", async () => {
const { rte, form } = await createTestRenderer(
{
Mutation: {
createComment: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: ERROR_CODES.COMMENTING_DISABLED,
});
}),
},
Query: {
settings: createSinonStub(
s => s.onFirstCall().returns(settings),
s =>
s.onSecondCall().returns({
...settings,
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
})
),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
form.props.onSubmit();
await waitForElement(() => within(form).getByText("commenting disabled"));
expect(rte.props.disabled).toBe(true);
expect(within(form).getByText("Submit").props.disabled).toBe(true);
});
it("handle story closed", async () => {
let returnStory = stories[0];
const { rte, form } = await createTestRenderer(
{
Mutation: {
createComment: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: ERROR_CODES.STORY_CLOSED,
});
}),
},
Query: {
story: sinon.stub().callsFake(() => returnStory),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
form.props.onSubmit();
// Change the story that we return to be closed.
returnStory = { ...stories[0], isClosed: true };
await waitForElement(() => within(form).getByText("Story is closed"));
expect(rte.props.disabled).toBe(true);
expect(within(form).getByText("Submit").props.disabled).toBe(true);
});
@@ -4,15 +4,16 @@ import timekeeper from "timekeeper";
import { ERROR_CODES } from "talk-common/errors";
import { InvalidRequestError } from "talk-framework/lib/errors";
import {
createSinonStub,
findParentWithType,
waitForElement,
within,
} from "talk-framework/testHelpers";
import RTE from "@coralproject/rte";
import { baseComment, settings, stories, users } from "../fixtures";
import create from "./create";
function createTestRenderer(
async function createTestRenderer(
resolver: any,
options: { muteNetworkErrors?: boolean } = {}
) {
@@ -21,17 +22,15 @@ function createTestRenderer(
Query: {
settings: sinon.stub().returns(settings),
me: sinon.stub().returns(users[0]),
story: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: stories[0].id, url: null })
.returns(stories[0])
),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
},
};
return create({
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
@@ -41,10 +40,39 @@ function createTestRenderer(
localRecord.setValue(true, "loggedIn");
},
});
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 } = createTestRenderer({
const { testRenderer, comment, rte, form } = await createTestRenderer({
Mutation: {
createCommentReply: sinon.stub().callsFake((_, data) => {
expect(data).toEqual({
@@ -71,29 +99,16 @@ it("post a reply", async () => {
}),
},
});
const streamLog = await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
const comment = within(streamLog).getByTestID("comment-comment-0");
// Open reply form.
within(comment)
.getByText("Reply", { selector: "button" })
.props.onClick();
const form = await waitForElement(() => within(comment).getByType("form"));
expect(within(comment).toJSON()).toMatchSnapshot("open reply form");
// Write reply .
testRenderer.root
.findByProps({ inputId: "comments-replyCommentForm-rte-comment-0" })
.props.onChange({ html: "<b>Hello world!</b>" });
rte.props.onChange({ html: "<b>Hello world!</b>" });
timekeeper.freeze(new Date(baseComment.createdAt));
form.props.onSubmit();
const commentReplyList = within(streamLog).getByTestID(
const commentReplyList = within(testRenderer.root).getByTestID(
"commentReplyList-comment-0"
);
@@ -110,7 +125,7 @@ it("post a reply", async () => {
});
it("post a reply and handle server error", async () => {
const { testRenderer } = createTestRenderer(
const { rte, form, comment } = await createTestRenderer(
{
Mutation: {
createCommentReply: sinon.stub().callsFake(() => {
@@ -120,24 +135,9 @@ it("post a reply and handle server error", async () => {
},
{ muteNetworkErrors: true }
);
const streamLog = await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
const comment = within(streamLog).getByTestID("comment-comment-0");
// Open reply form.
within(comment)
.getByText("Reply", { selector: "button" })
.props.onClick();
const form = await waitForElement(() => within(comment).getByType("form"));
expect(within(comment).toJSON()).toMatchSnapshot("open reply form");
// Write reply .
testRenderer.root
.findByProps({ inputId: "comments-replyCommentForm-rte-comment-0" })
.props.onChange({ html: "<b>Hello world!</b>" });
rte.props.onChange({ html: "<b>Hello world!</b>" });
timekeeper.freeze(new Date(baseComment.createdAt));
form.props.onSubmit();
@@ -145,3 +145,67 @@ it("post a reply and handle server error", async () => {
// Look for internal error being displayed.
await waitForElement(() => within(comment).getByText("INTERNAL_ERROR"));
});
it("handle disabled commenting error", async () => {
let returnSettings = settings;
const { rte, form } = await createTestRenderer(
{
Mutation: {
createCommentReply: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: ERROR_CODES.COMMENTING_DISABLED,
});
}),
},
Query: {
settings: sinon.stub().callsFake(() => returnSettings),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
form.props.onSubmit();
// Change the settings that we return to be closed.
returnSettings = {
...settings,
disableCommenting: {
enabled: true,
message: "commenting disabled",
},
};
await waitForElement(() => within(form).getByText("commenting disabled"));
expect(rte.props.disabled).toBe(true);
expect(within(form).getByText("Submit").props.disabled).toBe(true);
});
it("handle story closed error", async () => {
let returnStory = stories[0];
const { rte, form } = await createTestRenderer(
{
Mutation: {
createCommentReply: sinon.stub().callsFake(() => {
throw new InvalidRequestError({
code: ERROR_CODES.STORY_CLOSED,
});
}),
},
Query: {
story: sinon.stub().callsFake(() => returnStory),
},
},
{ muteNetworkErrors: true }
);
rte.props.onChange({ html: "abc" });
form.props.onSubmit();
// Change the story that we return to be closed.
returnStory = { ...stories[0], isClosed: true };
await waitForElement(() => within(form).getByText("Story is closed"));
expect(rte.props.disabled).toBe(true);
expect(within(form).getByText("Submit").props.disabled).toBe(true);
});
@@ -1,41 +1,44 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import {
createSinonStub,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { waitForElement, within } from "talk-framework/testHelpers";
import { settings, stories } from "../fixtures";
import create from "./create";
let testRenderer: ReactTestRenderer;
beforeEach(() => {
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) {
const resolvers = {
...resolver,
Query: {
story: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: stories[0].id, url: null })
.returns(stories[0])
),
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
},
};
({ testRenderer } = create({
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
},
}));
});
});
it("renders app with comment stream", async () => {
return {
testRenderer,
context,
};
}
it("renders comment stream", async () => {
const { testRenderer } = await createTestRenderer();
await waitForElement(() =>
within(testRenderer.root).getByTestID("comments-stream-log")
);
+6
View File
@@ -11,6 +11,12 @@ export const settings = {
enabled: false,
content: "",
},
disableCommenting: {
enabled: false,
message: "Commenting has been disabled",
},
closedAt: null,
closedMessage: "Story is closed",
auth: {
integrations: {
facebook: {
+8
View File
@@ -3,6 +3,14 @@ export enum ERROR_TYPES {
}
export enum ERROR_CODES {
/**
* STORY_CLOSED is used when submitting a comment on a closed story.
*/
STORY_CLOSED = "STORY_CLOSED",
/**
* COMMENTING_DISABLED is used when submitting a comment while commenting has been disabled.
*/
COMMENTING_DISABLED = "COMMENTING_DISABLED",
/**
* COMMENT_BODY_TOO_SHORT is used when a submitted comment body is too short.
*/
+16
View File
@@ -189,6 +189,22 @@ export class TalkError extends VError {
}
}
export class CommentingDisabledError extends TalkError {
constructor() {
super({
code: ERROR_CODES.COMMENTING_DISABLED,
});
}
}
export class StoryClosedError extends TalkError {
constructor() {
super({
code: ERROR_CODES.STORY_CLOSED,
});
}
}
export class CommentBodyTooShortError extends TalkError {
constructor(min: number) {
super({
+2
View File
@@ -1,6 +1,8 @@
import { ERROR_CODES } from "talk-common/errors";
export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
COMMENTING_DISABLED: "error-commentingDisabled",
STORY_CLOSED: "error-storyClosed",
COMMENT_BODY_TOO_SHORT: "error-commentBodyTooShort",
COMMENT_BODY_EXCEEDS_MAX_LENGTH: "error-commentBodyExceedsMaxLength",
STORY_URL_NOT_PERMITTED: "error-storyURLNotPermitted",
@@ -7,7 +7,10 @@ import { storyModerationInputResolver } from "./ModerationQueues";
export const Story: GQLStoryTypeResolver<story.Story> = {
comments: (s, input, ctx) => ctx.loaders.Comments.forStory(s.id, input),
isClosed: () => false,
isClosed: (s, input, ctx) => {
const closedAt = getStoryClosedAt(ctx.tenant, s);
return !!closedAt && new Date() >= closedAt;
},
closedAt: (s, input, ctx) => getStoryClosedAt(ctx.tenant, s),
commentActionCounts: s => decodeActionCounts(s.commentCounts.action),
commentCounts: s => s.commentCounts.status,
+2
View File
@@ -1,3 +1,5 @@
error-commentingDisabled = Commenting has been disabled tenant wide.
error-storyClosed = Story is currently closed for commenting.
error-commentBodyTooShort = Comment body must have at least {$min} characters.
error-commentBodyExceedsMaxLength =
Comment body exceeds maximum length of {$max} characters.
@@ -1,3 +1,4 @@
import { CommentingDisabledError } from "talk-server/errors";
import { ModerationSettings } from "talk-server/models/settings";
import {
IntermediateModerationPhase,
@@ -16,7 +17,6 @@ export const commentingDisabled: IntermediateModerationPhase = ({
testDisabledCommenting(tenant) ||
(story.settings && testDisabledCommenting(story.settings))
) {
// TODO: (wyattjoh) return better error.
throw new Error("commenting has been disabled tenant wide");
throw new CommentingDisabledError();
}
};
@@ -1,3 +1,4 @@
import { StoryClosedError } from "talk-server/errors";
import {
IntermediateModerationPhase,
IntermediatePhaseResult,
@@ -11,7 +12,6 @@ export const storyClosed: IntermediateModerationPhase = ({
}): IntermediatePhaseResult | void => {
const closedAt = getStoryClosedAt(tenant, story);
if (closedAt && closedAt.valueOf() <= Date.now()) {
// TODO: (wyattjoh) return better error.
throw new Error("story is currently closed for commenting");
throw new StoryClosedError();
}
};