[next] Implement Comment History Pagination (#2008)

* refactor: profile

* feat: add pagination to comment history

* feat: add getMeSourceID helper

* feat: update profile in CreateCommentMutation

* fix: clear query response cache on mutation

* test: add integration tests for profile

* test: add unit tests
This commit is contained in:
Kiwi
2018-10-19 17:54:40 +00:00
committed by Wyatt Johnson
parent bc4d746291
commit 2e6237b9d9
27 changed files with 1521 additions and 132 deletions
+34
View File
@@ -242,6 +242,20 @@ export const assets = denormalizeAssets([
},
},
},
{
...baseAsset,
id: "asset-2",
url: "http://localhost/assets/asset-2",
comments: {
edges: [
{ node: comments[2], cursor: comments[2].createdAt },
{ node: comments[3], cursor: comments[3].createdAt },
],
pageInfo: {
hasNextPage: false,
},
},
},
]);
export const assetWithReplies = denormalizeAsset({
@@ -293,3 +307,23 @@ export const assetWithDeepestReplies = denormalizeAsset({
},
},
});
export const meWithComments = {
id: "me-with-comments",
username: "Markus",
comments: {
edges: [
{
node: { ...assets[0].comments.edges[0].node, asset: assets[0] },
cursor: comments[0].createdAt,
},
{
node: { ...assets[1].comments.edges[0].node, asset: assets[1] },
cursor: comments[1].createdAt,
},
],
pageInfo: {
hasNextPage: false,
},
},
};
@@ -0,0 +1,522 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`loads more comments 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
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"
>
Markus
</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]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "What's up?",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Hey!",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
</div>
</div>
</section>
</div>
`;
exports[`renders comment stream 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
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"
>
Markus
</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]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "What's up?",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-1"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<button
aria-controls="talk-profile-commentHistory-log"
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantOutlined Button-fullWidth"
disabled={false}
id="talk-profile-commentHistory-loadMore"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Load More
</button>
</div>
</div>
</section>
</div>
`;
@@ -0,0 +1,226 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders profile 1`] = `
<div
className="HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
2 Comments
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
My Profile
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
id="tabPane-PROFILE"
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"
>
Markus
</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]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-double"
>
<h1
className="Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Comment History
</h1>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Joining Too",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-1?commentID=comment-0"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
<div
className="HorizontalGutter-root HorizontalGutter-full"
>
<h1
className="Typography-root Typography-heading4 Typography-colorTextPrimary"
>
Story: title
</h1>
<time
className="Timestamp-root RelativeTime-root"
dateTime="2018-07-06T18:24:00.000Z"
title="2018-07-06T18:24:00.000Z"
>
2018-07-06T18:24:00.000Z
</time>
<div
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
<div
className="HTMLContent-root"
dangerouslySetInnerHTML={
Object {
"__html": "Hey!",
}
}
/>
</div>
<div
className="Flex-root Flex-flex Flex-itemGutter Flex-alignCenter Flex-directionRow"
>
<a
className="BaseButton-root Button-root Button-sizeRegular Button-colorRegular Button-variantUnderlined"
href="http://localhost/assets/asset-2?commentID=comment-2"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
target="_parent"
type="button"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
launch
</span>
<span>
View Conversation
</span>
</a>
</div>
</div>
</div>
</div>
</section>
</div>
`;
@@ -0,0 +1,13 @@
import createTopLevel, { CreateParams } from "../create";
export default function create(params: CreateParams) {
return createTopLevel({
...params,
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
localRecord.setValue("PROFILE", "activeTab");
params.initLocalState(localRecord, source, environment);
}
},
});
}
@@ -0,0 +1,92 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createSinonStub } from "talk-framework/testHelpers";
import { assets, comments, meWithComments } from "../fixtures";
import create from "./create";
let testRenderer: ReactTestRenderer;
beforeEach(() => {
const meStub = {
...meWithComments,
comments: createSinonStub(
s => s.throws(),
s =>
s.withArgs({ first: 5, orderBy: "CREATED_AT_DESC" }).returns({
edges: [
{
node: { ...comments[0], asset: assets[0] },
cursor: comments[0].createdAt,
},
{
node: { ...comments[1], asset: assets[0] },
cursor: comments[1].createdAt,
},
],
pageInfo: {
endCursor: comments[1].createdAt,
hasNextPage: true,
},
}),
s =>
s
.withArgs({
first: 10,
orderBy: "CREATED_AT_DESC",
after: comments[1].createdAt,
})
.returns({
edges: [
{
node: { ...comments[2], asset: assets[0] },
cursor: comments[2].createdAt,
},
],
pageInfo: {
endCursor: comments[2].createdAt,
hasNextPage: false,
},
})
),
};
const resolvers = {
Query: {
asset: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: assets[0].id, url: null })
.returns(assets[0])
),
me: sinon.stub().returns(meStub),
},
};
({ testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(assets[0].id, "assetID");
},
}));
});
it("renders comment stream", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
it("loads more comments", async () => {
testRenderer.root
.findByProps({ id: "talk-profile-commentHistory-loadMore" })
.props.onClick();
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});
@@ -0,0 +1,39 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { createSinonStub } from "talk-framework/testHelpers";
import { assets, meWithComments } from "../fixtures";
import create from "./create";
let testRenderer: ReactTestRenderer;
beforeEach(() => {
const resolvers = {
Query: {
asset: createSinonStub(
s => s.throws(),
s =>
s
.withArgs(undefined, { id: assets[0].id, url: null })
.returns(assets[0])
),
me: sinon.stub().returns(meWithComments),
},
};
({ testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
localRecord.setValue(assets[0].id, "assetID");
},
}));
});
it("renders profile", async () => {
// Wait for loading.
await timeout();
expect(testRenderer.toJSON()).toMatchSnapshot();
});