[CORL-166] Live Updates on Mod Queues (#2368)

* feat: client implementation of subscriptions and modqueue live counts

* fix: unit tests

* feat: live status update in moderation

* feat: live update of new comments in moderation

* chore: View New instead of View More

* feat: fade in transition for new comments

* chore: turn websocket proxy back on

* feat: initial server impl

* fix: make it work :-)

* fix: add box shadow

* chore: make test subscriptions only support 1 top level field following the spec

* fix: linting

* feat: support clientID

* fix: linting

* feat: support commentStatusUpdated subscription

* fix: disabled styles for approve and reject button

* feat: show moderated by system and update flags

* feat: support metrics recording on websocket connections

* fix: handle when same comment enters but leaves again
This commit is contained in:
Vinh
2019-06-21 17:01:07 +00:00
committed by Wyatt Johnson
parent 0e247ba383
commit 413f3e2f1e
111 changed files with 8230 additions and 5017 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,849 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`approves comment in rejected queue: count should be 1 1`] = `
<span
className="Counter-root Counter-colorInherit"
data-testid="moderate-navigation-reported-count"
>
<span
className="Counter-text"
>
1
</span>
</span>
`;
exports[`approves comment in rejected queue: dangling 1`] = `
<div
className="Card-root ModerateCard-root ModerateCard-dangling"
data-testid="moderate-comment-comment-0"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/0"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div>
<span
className="ModerateCard-story"
>
Story
</span>
:
<span
className="ModerateCard-storyTitle"
>
Finally a Cure for Cancer
</span>
<a
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
>
Moderate Story
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root ApproveButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`renders rejected queue with comments 1`] = `
<div
className="MainLayout-root"
data-testid="moderate-main-container"
>
<main
className="Moderate-main"
>
<div
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
>
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-0"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/0"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div>
<span
className="ModerateCard-story"
>
Story
</span>
:
<span
className="ModerateCard-storyTitle"
>
Finally a Cure for Cancer
</span>
<a
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
>
Moderate Story
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root RejectButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-1"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Ngoc
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Don't fool with me",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/1"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div>
<span
className="ModerateCard-story"
>
Story
</span>
:
<span
className="ModerateCard-storyTitle"
>
Finally a Cure for Cancer
</span>
<a
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
>
Moderate Story
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
3
</span>
</span>
</div>
<button
aria-controls="uuid-1"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root RejectButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
`;
exports[`renders rejected queue with comments and load more 1`] = `
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-2"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Max
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "I think I deserve better",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/2"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div>
<span
className="ModerateCard-story"
>
Story
</span>
:
<span
className="ModerateCard-storyTitle"
>
Finally a Cure for Cancer
</span>
<a
className="TextLink-root ModerateCard-link"
href="/admin/moderate/story-1"
onClick={[Function]}
>
Moderate Story
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Offensive
</span>
<span
className="Count-root"
>
1
</span>
</span>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
1
</span>
</span>
</div>
<button
aria-controls="uuid-2"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root RejectButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
`;
@@ -0,0 +1,255 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`all stories active search with no results 1`] = `
<div
aria-expanded={true}
aria-haspopup="listbox"
aria-label="Search or jump to story"
aria-owns="moderate-searchBar-listBox"
className="SubBar-root Bar-root"
data-testid="moderate-searchBar-container"
role="combobox"
>
<div
className="Box-root Flex-root SubBar-container Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<div
className="Backdrop-root Backdrop-active Bar-bumpZIndex"
/>
<form
aria-label="Stories"
className="Bar-bumpZIndex"
onMouseDown={[Function]}
onSubmit={[Function]}
role="search"
>
<div
className="Popover-root"
>
<div>
<div
className="Box-root Flex-root Field-root Flex-flex Flex-alignStretch"
>
<div
className="Box-root Flex-root Field-begin Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-md Field-searchIcon"
>
search
</span>
<div
className="Field-beginStories"
>
Stories:
</div>
</div>
<input
aria-autocomplete="list"
aria-controls="moderate-searchBar-listBox"
aria-label="Search or jump to story..."
autoComplete="off"
className="Field-input"
name="search"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="Use quotation marks around each search term (e.g. “team”, “St. Louis”)"
spellCheck={false}
value=""
/>
<div
className="Box-root Flex-root Field-end Flex-flex Flex-alignCenter"
>
<button
className="BaseButton-root Field-searchButton"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Search
</button>
</div>
</div>
</div>
<div
aria-hidden={false}
aria-labelledby="moderate-searchBar-popover-ariainfo"
id="moderate-searchBar-popover"
role="popup"
>
<div
className="Popover-popover Bar-popover Popover-bottom"
style={
Object {
"left": 0,
"opacity": 0,
"pointerEvents": "none",
"position": "absolute",
"top": 0,
}
}
>
<ul
className="Bar-listBox"
id="moderate-searchBar-listBox"
role="listbox"
>
<ul
aria-labelledby="moderate-searchBar-context-title"
className="Group-root"
id="moderate-searchBar-context"
role="group"
>
<li
className="Group-title"
id="moderate-searchBar-context-title"
>
Currently moderating
</li>
<li
aria-selected={false}
className="Option-root"
id="moderate-searchBar-listBoxOption-0"
onClick={[Function]}
role="option"
>
<a
className="Option-link"
href="/admin/moderate"
tabIndex={-1}
>
<div
className="Option-container"
>
<div
className="Option-title"
>
<div
className="AriaInfo-root"
>
Go to
</div>
<span>
All stories
</span>
</div>
<div
className="Option-details"
/>
</div>
</a>
</li>
</ul>
</ul>
</div>
</div>
</div>
</form>
</div>
</div>
`;
exports[`all stories active search with too many results 1`] = `
<li
aria-selected={false}
className="SeeAllOption-root"
id="moderate-searchBar-listBoxOption-3"
onClick={[Function]}
role="option"
>
<a
className="SeeAllOption-link"
href="/admin/stories?q=InterestingStory"
tabIndex={-1}
>
<span>
See all results
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm SeeAllOption-icon"
>
arrow_forward
</span>
</a>
</li>
`;
exports[`all stories renders search bar 1`] = `
<div
aria-expanded={false}
aria-haspopup="listbox"
aria-label="Search or jump to story"
aria-owns="moderate-searchBar-listBox"
className="SubBar-root Bar-root"
data-testid="moderate-searchBar-container"
role="combobox"
>
<div
className="Box-root Flex-root SubBar-container Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<div
className="Backdrop-root Bar-bumpZIndex"
/>
<form
aria-label="Stories"
className="Bar-bumpZIndex"
onMouseDown={[Function]}
onSubmit={[Function]}
role="search"
>
<div
className="Popover-root"
>
<div>
<div
className="Box-root Flex-root Field-root Flex-flex Flex-alignStretch"
>
<div
className="Box-root Flex-root Field-begin Flex-flex Flex-alignCenter"
>
<span
aria-hidden="true"
className="Icon-root Icon-md Field-searchIcon"
>
search
</span>
</div>
<input
aria-autocomplete="list"
aria-controls="moderate-searchBar-listBox"
aria-label="Search or jump to story..."
autoComplete="off"
className="Field-input Field-inputWithTitle"
name="search"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
placeholder="All stories"
spellCheck={false}
value=""
/>
<div
className="Box-root Flex-root Field-end Flex-flex Flex-alignCenter"
/>
</div>
</div>
<div
aria-hidden={true}
aria-labelledby="moderate-searchBar-popover-ariainfo"
id="moderate-searchBar-popover"
role="popup"
/>
</div>
</form>
</div>
</div>
`;
@@ -0,0 +1,586 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`approves single comment 1`] = `
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-0"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/0"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root ApproveButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`rejects single comment 1`] = `
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-0"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/0"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root RejectButton-invert"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
`;
exports[`renders single comment view 1`] = `
<div
data-testid="single-moderate-container"
>
<div
className="SubBar-root SingleModerate-subBar SubBar-gutterBegin SubBar-gutterEnd"
>
<div
className="Box-root Flex-root SubBar-container Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<a
className="SingleModerate-subBarBegin"
href="/admin/moderate/"
onClick={[Function]}
>
Go to moderation queues
</a>
<div
className="SingleModerate-subBarTitle"
>
Single Comment View
</div>
</div>
</div>
<div
className="SingleModerate-background"
/>
<div
className="MainLayout-root"
>
<main
className="SingleModerate-main"
>
<div
className="Box-root HorizontalGutter-root Queue-root HorizontalGutter-double"
>
<div
className="Card-root ModerateCard-root"
data-testid="moderate-comment-comment-0"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="ModerateCard-mainContainer"
>
<div
className="ModerateCard-topBar"
>
<div>
<span
className="Box-root Typography-root Typography-heading4 Typography-colorTextPrimary ModerateCard-username Username-root"
>
Isabelle
</span>
<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>
<button
className="BaseButton-root FeatureButton-root"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span>
Feature
</span>
</button>
</div>
</div>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary ModerateCard-content CommentContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "This is the last random sentence I will be writing and I am going to stop mid-sent",
}
}
/>
<div
className="ModerateCard-footer"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-full"
>
<div>
<a
className="TextLink-root ModerateCard-link"
href="http://localhost/comment/0"
target="_blank"
>
View Context
<span
aria-hidden="true"
className="Icon-root Icon-xs TextLink-icon"
>
open_in_new
</span>
</a>
</div>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Box-root Flex-root Flex-flex"
>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<span
className="Marker-root Marker-colorError Marker-variantRegular"
>
<span>
Spam
</span>
<span
className="Count-root"
>
2
</span>
</span>
</div>
<button
aria-controls="uuid-0"
aria-expanded={false}
className="BaseButton-root Button-root Button-sizeSmall Button-colorRegular Markers-detailsButtonColorRegular Button-variantRegular Markers-detailsButton"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
className="Markers-detailsText"
>
Details
</span>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
arrow_drop_down
</span>
</button>
</div>
</div>
</div>
</div>
</div>
<div
className="ModerateCard-separator"
/>
<div
className="Box-root Flex-root ModerateCard-aside ModerateCard-asideWithoutReplyTo Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionColumn gutter"
>
<div
className="ModerateCard-decision"
>
Decision
</div>
<div
className="Box-root Flex-root Flex-flex Flex-itemGutter gutter"
>
<button
aria-label="Reject"
className="BaseButton-root RejectButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg RejectButton-icon"
>
close
</span>
</button>
<button
aria-label="Approve"
className="BaseButton-root ApproveButton-root"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-lg ApproveButton-icon"
>
done
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
`;
@@ -0,0 +1,127 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`tab bar renders tab bar (empty queues) 1`] = `
<div
className="SubBar-root"
data-testid="moderate-tabBar-container"
>
<div
className="Box-root Flex-root SubBar-container Flex-flex Flex-justifyCenter Flex-alignCenter"
>
<nav
className="Navigation-root"
>
<ul
className="Navigation-ul"
>
<li
className="NavigationItem-root"
>
<a
className="NavigationItem-anchor NavigationItem-active"
href="/admin/moderate/reported"
onClick={[Function]}
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
flag
</span>
<span>
reported
</span>
<span
className="Counter-root Counter-colorInherit"
data-testid="moderate-navigation-reported-count"
>
<span
className="Counter-text"
>
0
</span>
</span>
</a>
</li>
<li
className="NavigationItem-root"
>
<a
className="NavigationItem-anchor"
href="/admin/moderate/pending"
onClick={[Function]}
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
access_time
</span>
<span>
Pending
</span>
<span
className="Counter-root Counter-colorInherit"
data-testid="moderate-navigation-pending-count"
>
<span
className="Counter-text"
>
0
</span>
</span>
</a>
</li>
<li
className="NavigationItem-root"
>
<a
className="NavigationItem-anchor"
href="/admin/moderate/unmoderated"
onClick={[Function]}
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
forum
</span>
<span>
unmoderated
</span>
<span
className="Counter-root Counter-colorInherit"
data-testid="moderate-navigation-unmoderated-count"
>
<span
className="Counter-text"
>
0
</span>
</span>
</a>
</li>
<li
className="NavigationItem-root"
>
<a
className="NavigationItem-anchor"
href="/admin/moderate/rejected"
onClick={[Function]}
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
cancel
</span>
<span>
rejected
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
`;
@@ -0,0 +1,123 @@
import { pureMerge } from "coral-common/utils";
import {
GQLComment,
GQLMODERATION_QUEUE,
GQLResolver,
SubscriptionToCommentEnteredModerationQueueResolver,
} from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
it("live update count", async () => {
const { testRenderer, subscriptionHandler } = await createTestRenderer();
const verifyCount = (queue: GQLMODERATION_QUEUE, no: number) => {
within(
within(testRenderer.root).getByTestID(
`moderate-navigation-${queue.toLocaleLowerCase()}-count`
)
).getByText(no.toString());
};
const commentEntered = (queue: GQLMODERATION_QUEUE, comment: GQLComment) => {
subscriptionHandler.dispatch<
SubscriptionToCommentEnteredModerationQueueResolver
>("commentEnteredModerationQueue", variables => {
if (variables.queue && variables.queue !== queue) {
return;
}
return {
queue,
comment,
};
});
};
const commentLeft = (queue: GQLMODERATION_QUEUE, comment: GQLComment) => {
subscriptionHandler.dispatch<
SubscriptionToCommentEnteredModerationQueueResolver
>("commentLeftModerationQueue", variables => {
if (variables.queue && variables.queue !== queue) {
return;
}
return {
queue,
comment,
};
});
};
await act(async () => {
await wait(() =>
expect(subscriptionHandler.has("commentEnteredModerationQueue")).toBe(
true
)
);
verifyCount(GQLMODERATION_QUEUE.REPORTED, 0);
verifyCount(GQLMODERATION_QUEUE.PENDING, 0);
verifyCount(GQLMODERATION_QUEUE.UNMODERATED, 0);
commentEntered(GQLMODERATION_QUEUE.REPORTED, reportedComments[0]);
verifyCount(GQLMODERATION_QUEUE.REPORTED, 1);
verifyCount(GQLMODERATION_QUEUE.PENDING, 0);
verifyCount(GQLMODERATION_QUEUE.UNMODERATED, 0);
commentEntered(GQLMODERATION_QUEUE.REPORTED, reportedComments[1]);
verifyCount(GQLMODERATION_QUEUE.REPORTED, 2);
verifyCount(GQLMODERATION_QUEUE.PENDING, 0);
verifyCount(GQLMODERATION_QUEUE.UNMODERATED, 0);
commentEntered(GQLMODERATION_QUEUE.PENDING, reportedComments[2]);
commentEntered(GQLMODERATION_QUEUE.PENDING, reportedComments[2]);
commentLeft(GQLMODERATION_QUEUE.REPORTED, reportedComments[1]);
verifyCount(GQLMODERATION_QUEUE.REPORTED, 1);
verifyCount(GQLMODERATION_QUEUE.PENDING, 2);
verifyCount(GQLMODERATION_QUEUE.UNMODERATED, 0);
});
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,554 @@
import { pureMerge } from "coral-common/utils";
import {
GQLCOMMENT_STATUS,
GQLResolver,
ModerationQueueToCommentsResolver,
MutationToApproveCommentResolver,
MutationToRejectCommentResolver,
} from "coral-framework/schema";
import {
act,
createMutationResolverStub,
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
it("renders empty reported queue", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer();
const { getByText } = within(testRenderer.root);
await waitForElement(() => getByText("no more reported", { exact: false }));
});
});
it("renders empty pending queue", async () => {
replaceHistoryLocation("http://localhost/admin/moderate/pending");
const { testRenderer } = await createTestRenderer();
const { getByText } = within(testRenderer.root);
await waitForElement(() => getByText("no more pending", { exact: false }));
});
it("renders empty unmoderated queue", async () => {
replaceHistoryLocation("http://localhost/admin/moderate/unmoderated");
const { testRenderer } = await createTestRenderer();
const { getByText } = within(testRenderer.root);
await waitForElement(() =>
getByText("comments have been moderated", { exact: false })
);
});
it("renders empty rejected queue", async () => {
replaceHistoryLocation("http://localhost/admin/moderate/rejected");
const { testRenderer } = await createTestRenderer();
const { getByText } = within(testRenderer.root);
await waitForElement(() =>
getByText("no rejected comments", { exact: false })
);
});
it("renders reported queue with comments", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}) as any,
},
}),
},
}),
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
});
it("renders reported queue with comments", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}),
},
}),
},
}),
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
});
it("show details of comment with flags", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 1,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[0].createdAt,
hasNextPage: false,
},
};
}),
},
}),
},
}),
});
const { getByTestID } = within(testRenderer.root);
const reported = await waitForElement(() =>
getByTestID(`moderate-comment-${reportedComments[0].id}`)
);
expect(
within(reported).queryByText(
reportedComments[0].flags.nodes[0].additionalDetails!
)
).toBeNull();
within(reported)
.getByText("Details", { selector: "button" })
.props.onClick();
within(reported).getByText(
reportedComments[0].flags.nodes[0].additionalDetails!
);
});
});
it("shows a moderate story", async () => {
await act(async () => {
const {
testRenderer,
context: { transitionControl },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}) as any,
},
}),
},
}),
});
const moderateStory = await waitForElement(
() => within(testRenderer.root).getAllByText("Moderate Story")[0]
);
transitionControl.allowTransition = false;
moderateStory.props.onClick({});
// Expect a routing request was made to the right url.
expect(transitionControl.history[0].pathname).toBe(
`/admin/moderate/${reportedComments[0].story.id}`
);
});
});
it("renders reported queue with comments and load more", async () => {
await act(async () => {
const moderationQueuesStub = pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<ModerationQueueToCommentsResolver>(
({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: true,
},
};
default:
expectAndFail(variables).toEqual({
first: 10,
after: reportedComments[1].createdAt,
});
return {
edges: [
{
node: reportedComments[2],
cursor: reportedComments[2].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[2].createdAt,
hasNextPage: false,
},
};
}
}
) as any,
},
});
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () => moderationQueuesStub,
},
}),
});
const moderateContainer = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-container")
);
const { getByText, getAllByTestID, getByTestID } = within(
moderateContainer
);
// Get previous count of comments.
const previousCount = getAllByTestID(/^moderate-comment-.*$/).length;
const loadMore = await waitForElement(() => getByText("Load More"));
loadMore.props.onClick();
// Wait for load more to disappear.
await waitUntilThrow(() => getByText("Load More"));
// Verify we have one more item now.
const comments = getAllByTestID(/^moderate-comment-.*$/);
expect(comments.length).toBe(previousCount + 1);
// Verify last one added was our new one
expect(comments[comments.length - 1].props["data-testid"]).toBe(
`moderate-comment-${reportedComments[2].id}`
);
// Snapshot of added comment.
expect(
toJSON(getByTestID(`moderate-comment-${reportedComments[2].id}`))
).toMatchSnapshot();
});
});
it("approves comment in reported queue", async () => {
await act(async () => {
const approveCommentStub = createMutationResolverStub<
MutationToApproveCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
});
return {
comment: {
id: reportedComments[0].id,
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
{
node: {
id: "mod-action",
author: {
id: viewer.id,
username: viewer.username,
},
},
},
],
},
},
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
};
});
const moderationQueuesStub = pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<ModerationQueueToCommentsResolver>(
({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}
) as any,
},
});
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () => moderationQueuesStub,
},
Mutation: {
approveComment: approveCommentStub,
},
}),
});
const testID = `moderate-comment-${reportedComments[0].id}`;
const { getByTestID } = within(testRenderer.root);
const comment = await waitForElement(() => getByTestID(testID));
const ApproveButton = await waitForElement(() =>
within(comment).getByLabelText("Approve")
);
ApproveButton.props.onClick();
// Snapshot dangling state of comment.
expect(toJSON(comment)).toMatchSnapshot("dangling");
// Wait until comment is gone.
await waitUntilThrow(() => getByTestID(testID));
expect(approveCommentStub.called).toBe(true);
// Count should have been updated.
expect(
toJSON(getByTestID("moderate-navigation-reported-count"))
).toMatchSnapshot("count should be 1");
});
});
it("rejects comment in reported queue", async () => {
await act(async () => {
const rejectCommentStub = createMutationResolverStub<
MutationToRejectCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
});
return {
comment: {
id: reportedComments[0].id,
status: GQLCOMMENT_STATUS.REJECTED,
statusHistory: {
edges: [
{
node: {
id: "mod-action",
author: {
id: viewer.id,
username: viewer.username,
},
},
},
],
},
},
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
};
});
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}) as any,
},
}),
},
Mutation: {
rejectComment: rejectCommentStub,
},
}),
});
const testID = `moderate-comment-${reportedComments[0].id}`;
const { getByTestID } = within(testRenderer.root);
const comment = await waitForElement(() => getByTestID(testID));
const RejectButton = await waitForElement(() =>
within(comment).getByLabelText("Reject")
);
RejectButton.props.onClick();
// Snapshot dangling state of comment.
expect(toJSON(comment)).toMatchSnapshot("dangling");
// Wait until comment is gone.
await waitUntilThrow(() => getByTestID(testID));
expect(rejectCommentStub.called).toBe(true);
// Count should have been updated.
expect(
toJSON(getByTestID("moderate-navigation-reported-count"))
).toMatchSnapshot("count should be 1");
});
});
@@ -0,0 +1,139 @@
import { pureMerge } from "coral-common/utils";
import {
GQLMODERATION_QUEUE,
GQLResolver,
SubscriptionToCommentEnteredModerationQueueResolver,
SubscriptionToCommentLeftModerationQueueResolver,
} from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation(`http://localhost/admin/moderate/reported`);
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-main-container")
);
return { testRenderer, context, container, subscriptionHandler };
}
it("allows viewing new when new comments come in", async () => {
const { subscriptionHandler, container } = await createTestRenderer();
const commentData = reportedComments[0];
expect(subscriptionHandler.has("commentEnteredModerationQueue")).toBe(true);
subscriptionHandler.dispatch<
SubscriptionToCommentEnteredModerationQueueResolver
>("commentEnteredModerationQueue", variables => {
if (
variables.storyID !== null ||
variables.queue !== GQLMODERATION_QUEUE.REPORTED
) {
return;
}
return {
queue: GQLMODERATION_QUEUE.REPORTED,
comment: commentData,
};
});
const viewNewButton = await waitForElement(() =>
within(container).getByText("View 1 new", {
exact: false,
selector: "button",
})
);
act(() => {
viewNewButton.props.onClick();
});
// View New Button should disappear.
expect(() => within(container).getByText(/View \d+ new/)).toThrow();
// New comment should appear.
within(container).getByTestID(`moderate-comment-${commentData.id}`);
});
it("recognizes when same comment enters and leaves again", async () => {
const { subscriptionHandler, container } = await createTestRenderer();
const commentData = reportedComments[0];
expect(subscriptionHandler.has("commentEnteredModerationQueue")).toBe(true);
subscriptionHandler.dispatch<
SubscriptionToCommentEnteredModerationQueueResolver
>("commentEnteredModerationQueue", variables => {
if (
variables.storyID !== null ||
variables.queue !== GQLMODERATION_QUEUE.REPORTED
) {
return;
}
return {
queue: GQLMODERATION_QUEUE.REPORTED,
comment: commentData,
};
});
await waitForElement(() =>
within(container).getByText(/View \d+ new/, {
exact: false,
selector: "button",
})
);
subscriptionHandler.dispatch<
SubscriptionToCommentLeftModerationQueueResolver
>("commentLeftModerationQueue", variables => {
if (
variables.storyID !== null ||
variables.queue !== GQLMODERATION_QUEUE.REPORTED
) {
return;
}
return {
queue: GQLMODERATION_QUEUE.REPORTED,
comment: commentData,
};
});
// View New Button should disappear.
expect(() => within(container).getByText(/View \d+ new/)).toThrow();
});
@@ -0,0 +1,126 @@
import { pureMerge } from "coral-common/utils";
import {
GQLCOMMENT_STATUS,
GQLMODERATION_QUEUE,
GQLResolver,
ModerationQueueToCommentsResolver,
SubscriptionToCommentLeftModerationQueueResolver,
} from "coral-framework/schema";
import {
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
const commentData = reportedComments[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation(`http://localhost/admin/moderate/reported`);
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 1,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: commentData,
cursor: commentData.createdAt,
},
],
pageInfo: {
endCursor: commentData.createdAt,
hasNextPage: false,
},
};
}),
},
}),
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-main-container")
);
const comment = within(container).getByTestID(
`moderate-comment-${commentData.id}`
);
return { testRenderer, context, container, comment, subscriptionHandler };
}
it("update comment status live", async () => {
const { subscriptionHandler, comment } = await createTestRenderer();
expect(subscriptionHandler.has("commentLeftModerationQueue")).toBe(true);
expect(() =>
within(comment).getByText("Moderated By", { exact: false })
).toThrow();
subscriptionHandler.dispatch<
SubscriptionToCommentLeftModerationQueueResolver
>("commentLeftModerationQueue", variables => {
if (
variables.storyID !== null ||
variables.queue !== GQLMODERATION_QUEUE.REPORTED
) {
return;
}
return {
queue: GQLMODERATION_QUEUE.REPORTED,
comment: pureMerge<typeof commentData>(commentData, {
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
{
node: {
id: "mod-action-1",
moderator: users.moderators[0],
status: GQLCOMMENT_STATUS.APPROVED,
},
},
],
},
}),
};
});
await waitForElement(() =>
within(comment).getByText("Moderated By", { exact: false })
);
});
@@ -0,0 +1,314 @@
import { pureMerge } from "coral-common/utils";
import {
GQLCOMMENT_STATUS,
GQLResolver,
MutationToApproveCommentResolver,
} from "coral-framework/schema";
import {
createMutationResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
rejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
beforeEach(() => {
replaceHistoryLocation(`http://localhost/admin/moderate/rejected`);
});
it("renders rejected queue with comments", async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables }) => {
expectAndFail(variables).toEqual({
first: 5,
status: "REJECTED",
storyID: null,
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
};
},
},
}),
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
it("shows a moderate story", async () => {
const {
testRenderer,
context: { transitionControl },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables }) => {
expectAndFail(variables).toEqual({
first: 5,
status: "REJECTED",
storyID: null,
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
};
},
},
}),
});
const moderateStory = await waitForElement(
() => within(testRenderer.root).getAllByText("Moderate Story")[0]
);
transitionControl.allowTransition = false;
moderateStory.props.onClick({});
// Expect a routing request was made to the right url.
expect(transitionControl.history[0].pathname).toBe(
`/admin/moderate/${reportedComments[0].story.id}`
);
});
it("renders rejected queue with comments and load more", async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(variables).toEqual({
first: 5,
status: GQLCOMMENT_STATUS.REJECTED,
storyID: null,
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: true,
},
};
default:
expectAndFail(variables).toEqual({
first: 10,
after: rejectedComments[1].createdAt,
status: GQLCOMMENT_STATUS.REJECTED,
storyID: null,
});
return {
edges: [
{
node: rejectedComments[2],
cursor: rejectedComments[2].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[2].createdAt,
hasNextPage: false,
},
};
}
},
},
}),
});
const moderateContainer = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-container")
);
const { getByText, getAllByTestID, getByTestID } = within(moderateContainer);
// Get previous count of comments.
const previousCount = getAllByTestID(/^moderate-comment-.*$/).length;
const loadMore = await waitForElement(() => getByText("Load More"));
loadMore.props.onClick();
// Wait for load more to disappear.
await waitUntilThrow(() => getByText("Load More"));
// Verify we have one more item now.
const comments = getAllByTestID(/^moderate-comment-.*$/);
expect(comments.length).toBe(previousCount + 1);
// Verify last one added was our new one
expect(comments[comments.length - 1].props["data-testid"]).toBe(
`moderate-comment-${rejectedComments[2].id}`
);
// Snapshot of added comment.
expect(
toJSON(getByTestID(`moderate-comment-${rejectedComments[2].id}`))
).toMatchSnapshot();
});
it("approves comment in rejected queue", async () => {
const approveCommentStub = createMutationResolverStub<
MutationToApproveCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: rejectedComments[0].id,
commentRevisionID: rejectedComments[0].revision.id,
});
return {
comment: {
id: rejectedComments[0].id,
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
{
node: {
id: "mod-action",
author: {
id: viewer.id,
username: viewer.username,
},
},
},
],
},
},
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
};
});
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables }) => {
expectAndFail(variables).toEqual({
first: 5,
status: "REJECTED",
storyID: null,
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
};
},
},
Mutation: {
approveComment: approveCommentStub,
},
}),
});
const testID = `moderate-comment-${rejectedComments[0].id}`;
const { getByTestID } = within(testRenderer.root);
const comment = await waitForElement(() => getByTestID(testID));
const ApproveButton = await waitForElement(() =>
within(comment).getByLabelText("Approve")
);
ApproveButton.props.onClick();
// Snapshot dangling state of comment.
expect(toJSON(getByTestID(testID))).toMatchSnapshot("dangling");
// Wait until comment is gone.
await waitUntilThrow(() => getByTestID(testID));
expect(approveCommentStub.called).toBe(true);
// Count should have been updated.
expect(
toJSON(getByTestID("moderate-navigation-reported-count"))
).toMatchSnapshot("count should be 1");
});
@@ -0,0 +1,264 @@
import { noop } from "lodash";
import { ReactTestInstance, ReactTestRenderer } from "react-test-renderer";
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
findParentWithType,
replaceHistoryLocation,
wait,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
emptyStories,
settings,
stories,
storyConnection,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
const openSearchBar = async (testRenderer: ReactTestRenderer) => {
await act(async () => {
await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-searchBar-container")
);
});
const searchBar = within(testRenderer.root).getByTestID(
"moderate-searchBar-container"
);
const textField = within(searchBar).getByLabelText(
"Search or jump to story..."
);
const form = findParentWithType(textField, "form")!;
act(() => textField.props.onFocus({}));
return { searchBar, textField, form };
};
describe("all stories", () => {
it("renders search bar", async () => {
let searchBar: ReactTestInstance;
await act(async () => {
const { testRenderer } = await createTestRenderer();
searchBar = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-searchBar-container")
);
});
expect(within(searchBar!).toJSON()).toMatchSnapshot();
});
describe("active", () => {
it("search with no results", async () => {
const query = "InterestingStory";
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ variables }) => {
expectAndFail(variables.query).toBe(query);
return emptyStories;
},
},
}),
});
const { searchBar, textField, form } = await openSearchBar(testRenderer);
expect(within(searchBar).toJSON()).toMatchSnapshot();
await act(async () => {
// Search for sth.
textField.props.onChange(query);
form.props.onSubmit();
// Ensure no results message is shown.
await wait(() =>
within(searchBar).getByText("No results", { exact: false })
);
});
act(() => {
// Blurring should close the listbox.
textField.props.onBlur({});
});
expect(within(searchBar).queryByText("No results")).toBeNull();
});
it("search with actual results", async () => {
const query = "InterestingStory";
const {
testRenderer,
context: { transitionControl },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ variables }) => {
expectAndFail(variables.query).toBe(query);
return storyConnection;
},
},
}),
});
transitionControl.allowTransition = false;
const { searchBar, textField, form } = await openSearchBar(testRenderer);
const story = storyConnection.edges[0].node;
let storyOption: ReactTestInstance;
await act(async () => {
// Search for sth.
textField.props.onChange(query);
form.props.onSubmit();
// Find the story in the search results.
storyOption = findParentWithType(
await waitForElement(() =>
within(searchBar).getByText(story.metadata!.title!, {
exact: false,
})
),
"li"
)!;
});
// Go to story.
storyOption!.props.onClick({ button: 0, preventDefault: noop });
// Expect a routing request was made to the right url.
expect(transitionControl.history[0].pathname).toBe(
`/admin/moderate/${story.id}`
);
});
it("search with too many results", async () => {
const query = "InterestingStory";
const {
testRenderer,
context: { transitionControl },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ variables }) => {
expectAndFail(variables.query).toBe(query);
return pureMerge<typeof storyConnection>(storyConnection, {
pageInfo: { hasNextPage: true },
});
},
},
}),
});
transitionControl.allowTransition = false;
const { searchBar, textField, form } = await openSearchBar(testRenderer);
let seeAllOption: ReactTestInstance;
await act(async () => {
// Search for sth.
textField.props.onChange(query);
form.props.onSubmit();
// Find see all options in the search results.
seeAllOption = findParentWithType(
await waitForElement(() =>
within(searchBar).getByText("See all results", { exact: false })
),
"li"
)!;
});
expect(within(seeAllOption!).toJSON()).toMatchSnapshot();
// Go to story.
seeAllOption!.props.onClick({ button: 0, preventDefault: noop });
// Expect a routing request was made to the right url.
expect(transitionControl.history[0].pathname).toBe("/admin/stories");
expect(transitionControl.history[0].search).toBe(`?q=${query}`);
});
});
});
describe("specified story", () => {
beforeEach(() => {
replaceHistoryLocation(`http://localhost/admin/moderate/${stories[0].id}`);
});
it("renders search bar", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
story: () => stories[0],
},
}),
});
const searchBar = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-searchBar-container")
);
const textField = within(searchBar).getByLabelText(
"Search or jump to story..."
);
expect(textField.props.placeholder).toBe(stories[0].metadata!.title);
});
});
it("shows moderate all option", async () => {
const {
testRenderer,
context: { transitionControl },
} = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
story: () => stories[0],
},
}),
});
transitionControl.allowTransition = false;
const { searchBar } = await openSearchBar(testRenderer);
// Find see all options in the search results.
const moderateAllOptions = findParentWithType(
await waitForElement(() =>
within(searchBar).getByText("Moderate all", { exact: false })
),
"li"
)!;
// Activate moderate all.
moderateAllOptions.props.onClick({ button: 0, preventDefault: noop });
// Expect a routing request was made to the right url.
expect(transitionControl.history[0].pathname).toBe("/admin/moderate");
});
});
@@ -0,0 +1,189 @@
import { pureMerge } from "coral-common/utils";
import {
GQLCOMMENT_STATUS,
MutationToApproveCommentResolver,
MutationToRejectCommentResolver,
QueryToCommentResolver,
} from "coral-framework/schema";
import { GQLResolver } from "coral-framework/schema";
import {
createMutationResolverStub,
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
rejectedComments,
reportedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
const comment = rejectedComments[0];
const commentStub = createQueryResolverStub<QueryToCommentResolver>(
({ variables }) => {
expectAndFail(variables).toEqual({ id: comment.id });
return reportedComments[0];
}
);
beforeEach(() => {
replaceHistoryLocation(
`http://localhost/admin/moderate/comment/${comment.id}`
);
});
it("renders single comment view", async () => {
const { testRenderer } = await createTestRenderer({
resolvers: {
Query: {
comment: commentStub,
},
},
});
const { getByTestID } = within(testRenderer.root);
const container = await waitForElement(() =>
getByTestID("single-moderate-container")
);
expect(toJSON(container)).toMatchSnapshot();
});
it("approves single comment", async () => {
const approveCommentStub = createMutationResolverStub<
MutationToApproveCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: comment.id,
commentRevisionID: comment.revision.id,
});
return {
comment: {
id: comment.id,
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
{
node: {
id: "mod-action",
author: {
id: viewer.id,
username: viewer.username,
},
},
},
],
},
},
moderationQueues: emptyModerationQueues,
};
});
const { testRenderer } = await createTestRenderer({
resolvers: {
Query: {
comment: commentStub,
},
Mutation: {
approveComment: approveCommentStub,
},
},
});
const { getByLabelText, getByTestID } = within(testRenderer.root);
const ApproveButton = await waitForElement(() => getByLabelText("Approve"));
ApproveButton.props.onClick();
expect(
toJSON(getByTestID(`moderate-comment-${comment.id}`))
).toMatchSnapshot();
expect(approveCommentStub.called).toBe(true);
});
it("rejects single comment", async () => {
const rejectCommentStub = createMutationResolverStub<
MutationToRejectCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: comment.id,
commentRevisionID: comment.revision.id,
});
return {
comment: {
id: comment.id,
status: GQLCOMMENT_STATUS.REJECTED,
statusHistory: {
edges: [
{
node: {
id: "mod-action",
author: {
id: viewer.id,
username: viewer.username,
},
},
},
],
},
},
moderationQueues: emptyModerationQueues,
};
});
const { testRenderer } = await createTestRenderer({
resolvers: {
Query: {
comment: commentStub,
},
Mutation: {
rejectComment: rejectCommentStub,
},
},
});
const { getByLabelText, getByTestID } = within(testRenderer.root);
const RejectButton = await waitForElement(() => getByLabelText("Reject"));
RejectButton.props.onClick();
expect(
toJSON(getByTestID(`moderate-comment-${comment.id}`))
).toMatchSnapshot();
expect(rejectCommentStub.called).toBe(true);
});
@@ -0,0 +1,99 @@
import { pureMerge } from "coral-common/utils";
import {
GQLCOMMENT_STATUS,
GQLResolver,
SubscriptionToCommentStatusUpdatedResolver,
} from "coral-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import { reportedComments, settings, users } from "../fixtures";
const viewer = users.admins[0];
const commentData = reportedComments[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation(
`http://localhost/admin/moderate/comment/${commentData.id}`
);
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
comment: () => commentData,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("single-moderate-container")
);
const comment = within(container).getByTestID(
`moderate-comment-${commentData.id}`
);
return { testRenderer, context, container, comment, subscriptionHandler };
}
it("update comment status live", async () => {
const { subscriptionHandler, comment } = await createTestRenderer();
expect(subscriptionHandler.has("commentStatusUpdated")).toBe(true);
expect(() =>
within(comment).getByText("Moderated By", { exact: false })
).toThrow();
subscriptionHandler.dispatch<SubscriptionToCommentStatusUpdatedResolver>(
"commentStatusUpdated",
variables => {
if (variables.id !== commentData.id) {
return;
}
return {
newStatus: GQLCOMMENT_STATUS.APPROVED,
comment: pureMerge<typeof commentData>(commentData, {
status: GQLCOMMENT_STATUS.APPROVED,
statusHistory: {
edges: [
{
node: {
id: "mod-action-1",
moderator: users.moderators[0],
status: GQLCOMMENT_STATUS.APPROVED,
},
},
],
},
}),
};
}
);
// When status was changed by another user, the moderated by info should appear.
await waitForElement(() =>
within(comment).getByText("Moderated By", { exact: false })
);
});
@@ -0,0 +1,69 @@
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
settings,
stories,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
it("passes storyID to the endpoints", async () => {
replaceHistoryLocation(`http://localhost/admin/moderate/${stories[0].id}`);
await act(async () => {
await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: ({ variables }) => {
expectAndFail(variables.storyID).toBe(stories[0].id);
return emptyModerationQueues;
},
comments: ({ variables }) => {
expectAndFail(variables.storyID).toBe(stories[0].id);
return emptyRejectedComments;
},
},
}),
});
});
});
@@ -0,0 +1,74 @@
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
within,
} from "coral-framework/testHelpers";
import create from "../create";
import {
emptyModerationQueues,
emptyRejectedComments,
settings,
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context, subscriptionHandler } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context, subscriptionHandler };
}
describe("tab bar", () => {
it("renders tab bar (empty queues)", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer();
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(
toJSON(getByTestID("moderate-tabBar-container"))
).toMatchSnapshot();
});
});
it("should not show moderate story link in comment cards", async () => {
await act(async () => {
const { testRenderer } = await createTestRenderer();
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(
within(testRenderer.root).queryByText("Moderate Story")
).toBeNull();
});
});
});