mirror of
https://github.com/wassname/talk.git
synced 2026-09-12 13:01:11 +08:00
[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:
@@ -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);
|
||||
});
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user