[CORL-159, CORL-160] Stream Config Tab (#2219)

* feat: Implement stream configuration tab

* feat: split profile & configure into separate bundles

* chore: better role logic

* fix+chore: add test cases, implement expectAndFail, refactor tests

* chore: add some comments

* chore: Update src/core/client/framework/lib/form/helpers.tsx

Co-Authored-By: cvle <vinh@wikiwi.io>

* feat: support new graphql mutations/schema

* fix: ci fixes

* fix: improvement to revision loading

* fix: updated some tests

* fix: adapt client to changes

* fix: remove obsolote isClosed in UpdateStory

* ci: increase no_output_timeout for build
This commit is contained in:
Kiwi
2019-03-18 22:07:52 +01:00
committed by GitHub
parent c738d9a355
commit 9eb5afbb2b
128 changed files with 2249 additions and 1081 deletions
@@ -16,7 +16,7 @@ async function createTestRenderer(
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
expectAndFail(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
@@ -35,7 +35,7 @@ function createTestRenderer(
},
Mutation: {
editComment: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
expectAndFail(data).toMatchObject({
input: {
commentID: commentWithReplies.id,
body: "Edited!",
@@ -25,7 +25,7 @@ async function createTestRenderer(
settings: sinon.stub().returns(settings),
me: sinon.stub().returns(users[0]),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
expectAndFail(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
@@ -72,7 +72,7 @@ it("post a comment", async () => {
const { rte, form, tabPane } = await createTestRenderer({
Mutation: {
createComment: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
expectAndFail(data).toMatchObject({
input: {
storyID: stories[0].id,
body: "<b>Hello world!</b>",
@@ -119,7 +119,7 @@ const postACommentAndHandleNonVisibleComment = async (
const { rte, form } = await createTestRenderer({
Mutation: {
createComment: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
expectAndFail(data).toMatchObject({
input: {
storyID: stories[0].id,
body: "<b>Hello world!</b>",
@@ -32,7 +32,7 @@ beforeEach(() => {
},
Mutation: {
createCommentReply: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
expectAndFail(data.input).toMatchObject({
storyID: storyWithDeepestReplies.id,
parentID: "comment-with-deepest-replies-5",
parentRevisionID: "revision-0",
@@ -23,7 +23,7 @@ async function createTestRenderer(
settings: sinon.stub().returns(settings),
me: sinon.stub().returns(users[0]),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
expectAndFail(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
@@ -75,7 +75,7 @@ it("post a reply", async () => {
const { testRenderer, comment, rte, form } = await createTestRenderer({
Mutation: {
createCommentReply: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
expectAndFail(data).toMatchObject({
input: {
storyID: stories[0].id,
parentID: stories[0].comments.edges[0].node.id,
@@ -127,7 +127,7 @@ it("post a reply and handle non-visible comment state", async () => {
const { comment, rte, form } = await createTestRenderer({
Mutation: {
createCommentReply: sinon.stub().callsFake((_, data) => {
expect(data).toMatchObject({
expectAndFail(data).toMatchObject({
input: {
storyID: stories[0].id,
parentID: stories[0].comments.edges[0].node.id,
@@ -9,7 +9,7 @@ function createTestRenderer() {
const resolvers = {
Query: {
story: sinon.stub().callsFake((_: any, data: any) => {
expect(data).toEqual({
expectAndFail(data).toEqual({
id: stories[0].id,
url: null,
});
@@ -20,7 +20,7 @@ function createTestRenderer() {
},
Mutation: {
createCommentReaction: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
expectAndFail(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
});
@@ -35,7 +35,7 @@ function createTestRenderer() {
};
}),
removeCommentReaction: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
expectAndFail(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
});
return {
@@ -14,7 +14,7 @@ async function createTestRenderer(
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expect(variables.id).toBe(stories[0].id);
expectAndFail(variables.id).toBe(stories[0].id);
return stories[0];
}),
...resolver.Query,
@@ -15,7 +15,7 @@ function createTestRenderer(
const resolvers = {
Query: {
story: sinon.stub().callsFake((_: any, data: any) => {
expect(data).toEqual({
expectAndFail(data).toEqual({
id: stories[0].id,
url: null,
});
@@ -27,7 +27,7 @@ function createTestRenderer(
},
Mutation: {
createCommentFlag: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
expectAndFail(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
reason: "COMMENT_REPORTED_OFFENSIVE",
@@ -42,7 +42,7 @@ function createTestRenderer(
};
}),
createCommentDontAgree: sinon.stub().callsFake((_: any, data: any) => {
expect(data.input).toMatchObject({
expectAndFail(data.input).toMatchObject({
commentID: stories[0].comments.edges[0].node.id,
commentRevisionID: stories[0].comments.edges[0].node.revision.id,
additionalDetails: "More info",
@@ -34,12 +34,12 @@ it("renders app with comment stream", async () => {
const commentsQueryStub = createSinonStub(
s =>
s.onFirstCall().callsFake((input: any) => {
expect(input).toEqual({ first: 5, orderBy: "CREATED_AT_DESC" });
expectAndFail(input).toEqual({ first: 5, orderBy: "CREATED_AT_DESC" });
return stories[0].comments;
}),
s =>
s.onSecondCall().callsFake((input: any) => {
expect(input).toEqual({
expectAndFail(input).toEqual({
after: null,
first: 5,
orderBy: "CREATED_AT_ASC",
@@ -49,8 +49,8 @@ it("renders app with comment stream", async () => {
);
const storyQueryStub = createSinonStub(s =>
s.callsFake((_: any, input: any) => {
expect(input.id).toEqual("story-1");
expect(input.url).toBeFalsy();
expectAndFail(input.id).toEqual("story-1");
expectAndFail(input.url).toBeFalsy();
return {
...stories[0],
comments: commentsQueryStub,
@@ -0,0 +1,169 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders configure 1`] = `
<section
aria-labelledby="tab-CONFIGURE"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-CONFIGURE"
role="tabpanel"
>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div
className="Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap"
>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Moderator
</span>
.
</div>
<div
className="Flex-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<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 Out
</button>
</div>
</div>
</div>
<form
autoComplete="off"
id="configure-form"
onSubmit={[Function]}
>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-justifySpaceBetween Flex-alignFlexStart"
>
<h1
className="Typography-root Typography-heading2 Typography-colorTextPrimary ConfigureCommentStream-heading"
>
Configure this Comment Stream
</h1>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorSuccess Button-variantFilled Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Apply
</button>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<div>
<div
className="CheckBox-root"
>
<input
checked={false}
className="CheckBox-input"
disabled={false}
id="moderation"
name="moderation"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<label
className="CheckBox-label"
htmlFor="moderation"
>
<span
className="CheckBox-labelSpan"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
<span>
Enable Pre-Moderation
</span>
</span>
</span>
</label>
</div>
<div
className="ToggleConfig-details"
>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary WidthLimitedDescription-root"
>
Moderators must approve any comment before it is published to this stream.
</p>
</div>
</div>
<div>
<div
className="CheckBox-root"
>
<input
checked={false}
className="CheckBox-input"
disabled={false}
id="premodLinksEnable"
name="premodLinksEnable"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<label
className="CheckBox-label"
htmlFor="premodLinksEnable"
>
<span
className="CheckBox-labelSpan"
>
<span
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
<span>
Pre-Moderate Comments Containing Links
</span>
</span>
</span>
</label>
</div>
<div
className="ToggleConfig-details"
>
<p
className="Typography-root Typography-detail Typography-colorTextSecondary WidthLimitedDescription-root"
>
Moderators must approve any comment that contains a link before it is published to this stream.
</p>
</div>
</div>
</div>
</form>
</div>
</section>
`;
@@ -0,0 +1,14 @@
import createTopLevel, { CreateParams } from "../create";
export default function create(params: CreateParams) {
return createTopLevel({
...params,
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
localRecord.setValue("CONFIGURE", "activeTab");
localRecord.setValue("jti", "accessTokenJTI");
params.initLocalState(localRecord, source, environment);
}
},
});
}
@@ -0,0 +1,45 @@
import sinon from "sinon";
import { waitForElement, within } from "talk-framework/testHelpers";
import { meAsModerator, settings, stories } from "../fixtures";
import create from "./create";
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean; status?: string } = {}
) {
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expectAndFail(variables).toEqual({ id: stories[0].id, url: null });
return stories[0];
}),
me: sinon.stub().returns(meAsModerator),
...resolver.Query,
},
...resolver,
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
},
});
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
return { testRenderer, tabPane };
}
it("renders configure", async () => {
const { tabPane } = await createTestRenderer();
expect(within(tabPane).toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,130 @@
import { cloneDeep, merge } from "lodash";
import sinon from "sinon";
import {
findParentWithType,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { meAsModerator, settings, stories } from "../fixtures";
import create from "./create";
async function createTestRenderer(
resolver: any = {},
options: { muteNetworkErrors?: boolean; status?: string } = {}
) {
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
story: sinon.stub().callsFake((_: any, variables: any) => {
expectAndFail(variables).toEqual({ id: stories[0].id, url: null });
return stories[0];
}),
me: sinon.stub().returns(meAsModerator),
...resolver.Query,
},
...resolver,
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(stories[0].id, "storyID");
},
});
const tabPane = await waitForElement(() =>
within(testRenderer.root).getByTestID("current-tab-pane")
);
const applyButton = within(tabPane).getByText("Apply");
const form = findParentWithType(applyButton, "form")!;
return { testRenderer, tabPane, applyButton, form };
}
it("change premod", async () => {
let storyRecord = cloneDeep(stories[0]);
const updateStorySettingsStub = sinon
.stub()
.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.moderation).toEqual("PRE");
storyRecord = merge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
});
const premodField = within(form).getByLabelText("Enable Pre-Moderation");
expect(applyButton.props.disabled).toBe(true);
// Let's enable premod.
premodField.props.onChange({});
expect(applyButton.props.disabled).toBe(false);
// Send form
form.props.onSubmit();
expect(applyButton.props.disabled).toBe(true);
expect(premodField.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(premodField.props.disabled).toBe(false);
});
// Should have successfully sent with server.
expect(updateStorySettingsStub.called).toBe(true);
});
it("change premod links", async () => {
let storyRecord = cloneDeep(stories[0]);
const updateStorySettingsStub = sinon
.stub()
.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.premodLinksEnable).toEqual(true);
storyRecord = merge(storyRecord, { settings: data.input.settings });
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const { form, applyButton } = await createTestRenderer({
Mutation: {
updateStorySettings: updateStorySettingsStub,
},
});
const premodLinksField = within(form).getByLabelText(
"Pre-Moderate Comments Containing Links"
);
expect(applyButton.props.disabled).toBe(true);
// Let's enable premod.
premodLinksField.props.onChange({});
expect(applyButton.props.disabled).toBe(false);
// Send form
form.props.onSubmit();
expect(applyButton.props.disabled).toBe(true);
expect(premodLinksField.props.disabled).toBe(true);
// Wait for submission to be finished
await wait(() => {
expect(premodLinksField.props.disabled).toBe(false);
});
// Should have successfully sent with server.
expect(updateStorySettingsStub.called).toBe(true);
});
+16 -2
View File
@@ -15,8 +15,12 @@ export const settings = {
enabled: false,
message: "Commenting has been disabled",
},
closeCommenting: {
auto: false,
message: "Story is closed",
timeout: null,
},
closedAt: null,
closedMessage: "Story is closed",
auth: {
integrations: {
facebook: {
@@ -278,7 +282,6 @@ export const baseStory = {
metadata: {
title: "title",
},
moderation: "POST",
isClosed: false,
comments: {
edges: [],
@@ -289,6 +292,10 @@ export const baseStory = {
commentCounts: {
totalVisible: 0,
},
settings: {
moderation: "POST",
premodLinksEnable: false,
},
};
export const stories = denormalizeStories([
@@ -372,9 +379,16 @@ export const storyWithDeepestReplies = denormalizeStory({
},
});
export const meAsModerator = {
id: "me-as-moderator",
username: "Moderator",
role: "MODERATOR",
};
export const meWithComments = {
id: "me-with-comments",
username: "Markus",
role: "COMMENTER",
comments: {
edges: [
{