mirror of
https://github.com/wassname/talk.git
synced 2026-07-19 11:28:50 +08:00
[next] Tests for new story filter and combo box (#2288)
* test: add tests for changes in stories page * test: search box and moderate specific story * fix: remaining snapshot * fix: test
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { BrowserProtocol, queryMiddleware } from "farce";
|
||||
import { createFarceRouter, createRender } from "found";
|
||||
import { createFarceRouter, ElementsRenderer } from "found";
|
||||
import { Resolver } from "found-relay";
|
||||
|
||||
import React, { StatelessComponent } from "react";
|
||||
import TransitionControl from "talk-framework/testHelpers/TransitionControl";
|
||||
|
||||
import { TalkContextConsumer } from "talk-framework/lib/bootstrap/TalkContext";
|
||||
|
||||
import routeConfig from "../routeConfig";
|
||||
import NotFound from "../routes/NotFound";
|
||||
|
||||
@@ -11,11 +13,16 @@ const Router = createFarceRouter({
|
||||
historyProtocol: new BrowserProtocol(),
|
||||
historyMiddlewares: [queryMiddleware],
|
||||
routeConfig,
|
||||
render: createRender({
|
||||
renderError: ({ error }) => (
|
||||
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
|
||||
),
|
||||
}),
|
||||
renderReady: ({ elements }) => (
|
||||
<>
|
||||
<ElementsRenderer elements={elements} />
|
||||
{// this enables router transition control when writing tests.
|
||||
process.env.NODE_ENV === "test" && <TransitionControl />}
|
||||
</>
|
||||
),
|
||||
renderError: ({ error }) => (
|
||||
<div>{error.status === 404 ? <NotFound /> : "Error"}</div>
|
||||
),
|
||||
});
|
||||
|
||||
const EntryContainer: StatelessComponent = () => (
|
||||
|
||||
@@ -27,7 +27,7 @@ const Moderate: StatelessComponent<Props> = ({
|
||||
}) => (
|
||||
<div data-testid="moderate-container">
|
||||
<ModerateSearchBarContainer story={story} allStories={allStories} />
|
||||
<SubBar data-testid="moderate-subBar-container">
|
||||
<SubBar data-testid="moderate-tabBar-container">
|
||||
<ModerateNavigationContainer
|
||||
moderationQueues={moderationQueues}
|
||||
story={story}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ exports[`renders correctly 1`] = `
|
||||
story={Object {}}
|
||||
/>
|
||||
<withPropsOnChange(SubBar)
|
||||
data-testid="moderate-subBar-container"
|
||||
data-testid="moderate-tabBar-container"
|
||||
>
|
||||
<Relay(ModerateNavigationContainer)
|
||||
moderationQueues={Object {}}
|
||||
|
||||
@@ -1,131 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`navigation bar renders navigation bar (empty queues) 1`] = `
|
||||
<div
|
||||
className="SubBar-root"
|
||||
data-testid="moderate-subBar-container"
|
||||
>
|
||||
<div
|
||||
className="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>
|
||||
`;
|
||||
|
||||
exports[`rejected queue accepts comment in rejected queue: count should be 1 1`] = `
|
||||
<span
|
||||
className="Counter-root Counter-colorInherit"
|
||||
@@ -1524,6 +1398,260 @@ exports[`reported queue renders reported queue with comments and load more 1`] =
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`search bar 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="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="Flex-root Field-root Flex-flex Flex-alignStretch"
|
||||
>
|
||||
<div
|
||||
className="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="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[`search bar 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[`search bar 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="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="Flex-root Field-root Flex-flex Flex-alignStretch"
|
||||
>
|
||||
<div
|
||||
className="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="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>
|
||||
`;
|
||||
|
||||
exports[`single comment view accepts single comment 1`] = `
|
||||
<div
|
||||
className="Card-root ModerateCard-root"
|
||||
@@ -1991,3 +2119,129 @@ exports[`single comment view renders single comment view 1`] = `
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`tab bar renders tab bar (empty queues) 1`] = `
|
||||
<div
|
||||
className="SubBar-root"
|
||||
data-testid="moderate-tabBar-container"
|
||||
>
|
||||
<div
|
||||
className="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>
|
||||
`;
|
||||
|
||||
@@ -12,20 +12,27 @@ import {
|
||||
createQueryResolverStub,
|
||||
createResolversStub,
|
||||
CreateTestRendererParams,
|
||||
findParentWithType,
|
||||
replaceHistoryLocation,
|
||||
toJSON,
|
||||
wait,
|
||||
waitForElement,
|
||||
waitUntilThrow,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import { noop } from "lodash";
|
||||
import { ReactTestRenderer } from "react-test-renderer";
|
||||
import create from "../create";
|
||||
import {
|
||||
emptyModerationQueues,
|
||||
emptyRejectedComments,
|
||||
emptyStories,
|
||||
rejectedComments,
|
||||
reportedComments,
|
||||
settings,
|
||||
stories,
|
||||
storyConnection,
|
||||
users,
|
||||
} from "../fixtures";
|
||||
|
||||
@@ -38,7 +45,7 @@ beforeEach(async () => {
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer } = create({
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
@@ -58,21 +65,236 @@ async function createTestRenderer(
|
||||
}
|
||||
},
|
||||
});
|
||||
return testRenderer;
|
||||
return { testRenderer, context };
|
||||
}
|
||||
|
||||
describe("navigation bar", () => {
|
||||
it("renders navigation bar (empty queues)", async () => {
|
||||
const testRenderer = await createTestRenderer();
|
||||
describe("search bar", () => {
|
||||
const openSearchBar = async (testRenderer: ReactTestRenderer) => {
|
||||
const searchBar = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("moderate-searchBar-container")
|
||||
);
|
||||
const textField = within(searchBar).getByLabelText(
|
||||
"Search or jump to story..."
|
||||
);
|
||||
const form = findParentWithType(textField, "form")!;
|
||||
textField.props.onFocus({});
|
||||
return { searchBar, textField, form };
|
||||
};
|
||||
|
||||
describe("all stories", () => {
|
||||
it("renders search bar", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
const 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();
|
||||
|
||||
// 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 })
|
||||
);
|
||||
|
||||
// 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
|
||||
);
|
||||
|
||||
// Search for sth.
|
||||
textField.props.onChange(query);
|
||||
form.props.onSubmit();
|
||||
|
||||
const story = storyConnection.edges[0].node;
|
||||
|
||||
// Find the story in the search results.
|
||||
const 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
|
||||
);
|
||||
|
||||
// Search for sth.
|
||||
textField.props.onChange(query);
|
||||
form.props.onSubmit();
|
||||
|
||||
// Find see all options in the search results.
|
||||
const 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 () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tab bar", () => {
|
||||
it("renders tab bar (empty queues)", async () => {
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
const { getByTestID } = within(testRenderer.root);
|
||||
await waitForElement(() => getByTestID("moderate-container"));
|
||||
expect(toJSON(getByTestID("moderate-subBar-container"))).toMatchSnapshot();
|
||||
expect(toJSON(getByTestID("moderate-tabBar-container"))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("moderating specific story", () => {
|
||||
it("passes storyID to the endpoints", async () => {
|
||||
replaceHistoryLocation(`http://localhost/admin/moderate/${stories[0].id}`);
|
||||
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;
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reported queue", () => {
|
||||
it("renders empty reported queue", async () => {
|
||||
const testRenderer = await createTestRenderer();
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
const { getByTestID } = within(testRenderer.root);
|
||||
|
||||
await waitForElement(() => getByTestID("moderate-container"));
|
||||
@@ -81,14 +303,14 @@ describe("reported queue", () => {
|
||||
|
||||
it("renders empty pending queue", async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/moderate/pending");
|
||||
const testRenderer = await createTestRenderer();
|
||||
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 { testRenderer } = await createTestRenderer();
|
||||
const { getByText } = within(testRenderer.root);
|
||||
await waitForElement(() =>
|
||||
getByText("comments have been moderated", { exact: false })
|
||||
@@ -97,7 +319,7 @@ describe("reported queue", () => {
|
||||
|
||||
it("renders empty rejected queue", async () => {
|
||||
replaceHistoryLocation("http://localhost/admin/moderate/rejected");
|
||||
const testRenderer = await createTestRenderer();
|
||||
const { testRenderer } = await createTestRenderer();
|
||||
const { getByText } = within(testRenderer.root);
|
||||
await waitForElement(() =>
|
||||
getByText("no rejected comments", { exact: false })
|
||||
@@ -105,7 +327,7 @@ describe("reported queue", () => {
|
||||
});
|
||||
|
||||
it("renders reported queue with comments", async () => {
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
moderationQueues: () =>
|
||||
@@ -191,7 +413,7 @@ describe("reported queue", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
moderationQueues: () => moderationQueuesStub,
|
||||
@@ -278,7 +500,7 @@ describe("reported queue", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
moderationQueues: () => moderationQueuesStub,
|
||||
@@ -333,7 +555,7 @@ describe("reported queue", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
moderationQueues: () =>
|
||||
@@ -400,7 +622,7 @@ describe("rejected queue", () => {
|
||||
});
|
||||
|
||||
it("renders rejected queue with comments", async () => {
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
comments: ({ variables }) => {
|
||||
@@ -435,7 +657,7 @@ describe("rejected queue", () => {
|
||||
});
|
||||
|
||||
it("renders rejected queue with comments and load more", async () => {
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
comments: ({ variables, callCount }) => {
|
||||
@@ -540,7 +762,7 @@ describe("rejected queue", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
comments: ({ variables }) => {
|
||||
@@ -613,7 +835,7 @@ describe("single comment view", () => {
|
||||
});
|
||||
|
||||
it("renders single comment view", async () => {
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: {
|
||||
Query: {
|
||||
comment: commentStub,
|
||||
@@ -644,7 +866,7 @@ describe("single comment view", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: {
|
||||
Query: {
|
||||
comment: commentStub,
|
||||
@@ -683,7 +905,7 @@ describe("single comment view", () => {
|
||||
};
|
||||
});
|
||||
|
||||
const testRenderer = await createTestRenderer({
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: {
|
||||
Query: {
|
||||
comment: commentStub,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { noop } from "lodash";
|
||||
import TestRenderer from "react-test-renderer";
|
||||
|
||||
import { pureMerge } from "talk-common/utils";
|
||||
@@ -36,7 +37,7 @@ beforeEach(async () => {
|
||||
async function createTestRenderer(
|
||||
params: CreateTestRendererParams<GQLResolver> = {}
|
||||
) {
|
||||
const { testRenderer } = create({
|
||||
const { testRenderer, context } = create({
|
||||
...params,
|
||||
resolvers: pureMerge(
|
||||
createResolversStub<GQLResolver>({
|
||||
@@ -62,7 +63,7 @@ async function createTestRenderer(
|
||||
const container = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("stories-container")
|
||||
);
|
||||
return { testRenderer, container };
|
||||
return { testRenderer, container, context };
|
||||
}
|
||||
|
||||
it("renders stories", async () => {
|
||||
@@ -81,6 +82,26 @@ it("renders empty stories", async () => {
|
||||
expect(within(container).toJSON()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("goes to moderation when clicking on title", async () => {
|
||||
const {
|
||||
container,
|
||||
context: { transitionControl },
|
||||
} = await createTestRenderer();
|
||||
|
||||
// Prevent router transitions.
|
||||
transitionControl.allowTransition = false;
|
||||
|
||||
const story = storyConnection.edges[0].node;
|
||||
within(container)
|
||||
.getByText(story.metadata!.title!)
|
||||
.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("filter by status", async () => {
|
||||
const { container } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
@@ -268,3 +289,24 @@ it("filter by search", async () => {
|
||||
within(container).getByText("could not find any", { exact: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("use searchFilter from url", async () => {
|
||||
const searchFilter = "CandyMountain";
|
||||
replaceHistoryLocation(`http://localhost/admin/stories?q=${searchFilter}`);
|
||||
const { container } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Query: {
|
||||
stories: ({ variables }) => {
|
||||
expectAndFail(variables.query).toBe(searchFilter);
|
||||
return emptyStories;
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const searchField = within(container).getByLabelText(
|
||||
"Search by story title",
|
||||
{ exact: false }
|
||||
);
|
||||
expect(searchField.props.value).toBe(searchFilter);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BrowserInfo } from "talk-framework/lib/browserInfo";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
import { PromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { TransitionControlData } from "talk-framework/testHelpers";
|
||||
import { UIContext } from "talk-ui/components";
|
||||
import { ClickFarAwayRegister } from "talk-ui/components/ClickOutside";
|
||||
|
||||
@@ -62,6 +63,9 @@ export interface TalkContext {
|
||||
|
||||
/** Clear session data. */
|
||||
clearSession: () => Promise<void>;
|
||||
|
||||
/** Controls router transitions (for tests) */
|
||||
transitionControl?: TransitionControlData;
|
||||
}
|
||||
|
||||
export const TalkReactContext = React.createContext<TalkContext>({} as any);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Location, Match, Router, withRouter } from "found";
|
||||
import React, { useEffect } from "react";
|
||||
import { withContext } from "talk-framework/lib/bootstrap";
|
||||
|
||||
interface Props {
|
||||
/** router is injected by `withRouter` HOC */
|
||||
router: Router;
|
||||
/** match is injected by `withRouter` HOC */
|
||||
match: Match;
|
||||
/** transitionControl is injected by `withContext` HOC */
|
||||
transitionControl: TransitionControlData | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* TransitionControlData allows controlling router transition.
|
||||
*/
|
||||
export interface TransitionControlData {
|
||||
/** allowTransition if set to false, will prevent router transitions from happening. */
|
||||
allowTransition: boolean;
|
||||
/** history contains all records of router transition requests. */
|
||||
history: Location[];
|
||||
}
|
||||
|
||||
const TransitionControl: React.FunctionComponent<Props> = props => {
|
||||
useEffect(() => {
|
||||
return props.router.addTransitionHook(location => {
|
||||
if (props.transitionControl) {
|
||||
props.transitionControl.history.push(location);
|
||||
if (!props.transitionControl.allowTransition) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
});
|
||||
}, []);
|
||||
return null;
|
||||
};
|
||||
|
||||
const enhanced = withContext(({ transitionControl }) => ({
|
||||
transitionControl,
|
||||
}))(withRouter(TransitionControl));
|
||||
|
||||
export default enhanced;
|
||||
@@ -6,6 +6,7 @@ import React from "react";
|
||||
import TestRenderer, { ReactTestRenderer } from "react-test-renderer";
|
||||
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
|
||||
|
||||
import { RequireProperty } from "talk-common/types";
|
||||
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
|
||||
import { PostMessageService } from "talk-framework/lib/postMessage";
|
||||
import { RestClient } from "talk-framework/lib/rest";
|
||||
@@ -77,7 +78,7 @@ export default function createTestRenderer<
|
||||
},
|
||||
});
|
||||
|
||||
const context: TalkContext = {
|
||||
const context: RequireProperty<TalkContext, "transitionControl"> = {
|
||||
relayEnvironment: environment,
|
||||
locales: ["en-US"],
|
||||
localeBundles: [
|
||||
@@ -94,6 +95,10 @@ export default function createTestRenderer<
|
||||
uuidGenerator: createUUIDGenerator(),
|
||||
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
|
||||
clearSession: () => Promise.resolve(),
|
||||
transitionControl: {
|
||||
allowTransition: true,
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
|
||||
let testRenderer: ReactTestRenderer;
|
||||
|
||||
@@ -36,3 +36,7 @@ export {
|
||||
CreateTestRendererParams,
|
||||
} from "./createTestRenderer";
|
||||
export { default as createResolversStub } from "./createResolversStub";
|
||||
export {
|
||||
TransitionControlData,
|
||||
default as TransitionControl,
|
||||
} from "./TransitionControl";
|
||||
|
||||
@@ -9,3 +9,20 @@ jest.mock("react-dom", () => ({
|
||||
...require.requireActual("react-dom"),
|
||||
createPortal: (node: any) => node,
|
||||
}));
|
||||
|
||||
jest.mock("popper.js", () => {
|
||||
const PopperJS = require.requireActual("popper.js");
|
||||
|
||||
return class Popper {
|
||||
public static placements = PopperJS.placements;
|
||||
|
||||
constructor() {
|
||||
return {
|
||||
// tslint:disable-next-line:no-empty
|
||||
destroy: () => {},
|
||||
// tslint:disable-next-line:no-empty
|
||||
scheduleUpdate: () => {},
|
||||
};
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U;
|
||||
|
||||
export type Sub<T, U> = Pick<T, Diff<keyof T, keyof U>>;
|
||||
|
||||
export type RequireProperty<T, P extends keyof T> = Omit<Partial<T>, P> &
|
||||
export type RequireProperty<T, P extends keyof T> = Omit<T, P> &
|
||||
Required<Pick<T, P>>;
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
declare module "found" {
|
||||
const ElementsRenderer: FunctionComponent<{
|
||||
elements: ReactElementOrGroup[];
|
||||
}>;
|
||||
}
|
||||
Reference in New Issue
Block a user