Skip to content

Commit

Permalink
test: add missing tests
Browse files Browse the repository at this point in the history
  • Loading branch information
adamborowski committed Jan 9, 2023
1 parent 1a9a367 commit 5d4db74
Show file tree
Hide file tree
Showing 5 changed files with 372 additions and 0 deletions.
70 changes: 70 additions & 0 deletions src/common/api/clients/fetchSearchClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createFetchSearchClient } from "./fetchSearchClient";
import z from "zod";
import { Adapter } from "./Adapter";

const createFetchMock = (data: unknown) =>
jest.fn().mockResolvedValue({ json: jest.fn().mockReturnValue(data) });
const schema = z.array(z.object({ test: z.string() }));
type ServerSchema = z.infer<typeof schema>;

describe("fetchSearchClient", () => {
it("should properly fetch and pass validation", async () => {
const fetchMock = createFetchMock([{ test: "a" }, { test: "b" }]);
const adapter: Adapter<ServerSchema, ServerSchema[number]> = {
parse: schema.parse,
serverToClient: (x) => x,
};
const client = createFetchSearchClient(
(query) => `mock/${query}`,
"token",
adapter,
fetchMock
);

const result = await client.search("givemerepo");

expect(fetchMock).toBeCalledWith(
"mock/givemerepo",
expect.objectContaining({
headers: expect.objectContaining({ Authorization: "Bearer token" }),
})
);
expect(result).toEqual([{ test: "a" }, { test: "b" }]);
});

it("should extract zod error", async () => {
const fetchMock = createFetchMock([{ foo: "a" }, { test: 3 }]);
const adapter: Adapter<ServerSchema, ServerSchema[number]> = {
parse: schema.parse,
serverToClient: (x) => x,
};
const client = createFetchSearchClient(
(query) => `mock/${query}`,
"token",
adapter,
fetchMock
);

await expect(client.search("givemerepo")).rejects.toThrowError(
"0,test: Required\n1,test: Expected string, received number"
);
});

it("should rethrow throw any other error other than zod", async () => {
const fetchMock = createFetchMock([{ test: "a" }, { test: "b" }]);
const adapter: Adapter<ServerSchema, ServerSchema[number]> = {
parse: schema.parse,
serverToClient: jest.fn().mockImplementation(() => {
throw new Error("Try me");
}),
};
const client = createFetchSearchClient(
(query) => `mock/${query}`,
"token",
adapter,
fetchMock
);

await expect(client.search("givemerepo")).rejects.toThrowError("Try me");
});
});
24 changes: 24 additions & 0 deletions src/common/state/cancelablePromise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { cancelableWithAbortController } from "./cancelablePromise";

describe("cancelableWithAbortController", () => {
it("should resolve when not cancelled", async () => {
const abortController = new AbortController();
const promise = cancelableWithAbortController(
abortController,
Promise.resolve("success")
);
await expect(promise).resolves.toEqual("success");
});

it("should not resolve and fire signal when cancelled", async () => {
const abortController = new AbortController();
const promise = cancelableWithAbortController(
abortController,
Promise.resolve("success")
);
promise.cancel();

expect(abortController.signal.aborted).toBeTruthy();
expect(promise.isCanceled()).toBeTruthy();
});
});
19 changes: 19 additions & 0 deletions src/features/repositories/components/AppLayout.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import { AppLayout } from "./AppLayout";
import { IntlProvider } from "react-intl";

describe("AppLayout", () => {
it("should render proper structure", () => {
render(
<AppLayout>
<div>Foo</div>
</AppLayout>,
{
wrapper: (props) => <IntlProvider locale="en" {...props} />,
}
);
expect(screen.getByText("Repositories Search Demo")).toBeVisible();
expect(screen.getByText("Foo")).toBeVisible();
expect(screen.getByTitle("Change theme")).toBeVisible();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { inMemorySearchClient, renderOptions } from "./__test__/test-common";
import { act, render, screen, waitFor } from "@testing-library/react";
import { RepositorySearchPageConnected } from "./RepositorySearchPage.connected";
import userEvent from "@testing-library/user-event";
import { withFailure } from "../../../../common/api/clients/development/withFailure";

describe("RepositorySearchPage.connected", () => {

it("should allow to search with query and re-search after changing the query", async () => {
render(
<RepositorySearchPageConnected searchClient={inMemorySearchClient} />,
renderOptions
);

// loading initial search results
expect(screen.getByTestId("spinner")).toBeVisible();
await waitFor(() => {
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

// it should have all 4 repositories having 'react' in name
expect(screen.getAllByTestId("repository-card")).toHaveLength(4);

// change query to "12" and search
userEvent.click(screen.getByDisplayValue("react"));
userEvent.keyboard("{selectall}12");
act(() => void userEvent.click(screen.getByText("Search")));

// now search button should be disabled and we should wait for results
expect(screen.getByText("Search")).toBeDisabled();
expect(screen.getByTestId("spinner")).toBeVisible();
await waitFor(() => {
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

// we should see only two repositories in results
expect(screen.getAllByTestId("repository-card")).toHaveLength(2);

// after successful search, change '12' to '123' in the query and try to search again
userEvent.click(screen.getByDisplayValue("12"));
userEvent.keyboard("3");
act(() => void userEvent.click(screen.getByText("Search")));
await waitFor(() => {
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

// now it should only show one repository
expect(screen.getAllByTestId("repository-card")).toHaveLength(1);

// if we don't change the query, clicking Search should do nothing
userEvent.click(screen.getByText("Search"));
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

it("should allow to search with query and re-search in error state", async () => {
render(
<RepositorySearchPageConnected
searchClient={withFailure(inMemorySearchClient, 1)}
/>,
renderOptions
);

// loading initial search results
expect(screen.getByTestId("spinner")).toBeVisible();
await waitFor(() => {
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

// we should see an error page
expect(screen.queryAllByTestId("repository-card")).toHaveLength(0);

// since we have an error, just click search again, it should be enabled
act(() => void userEvent.click(screen.getByText("Search")));

// now search button should be disabled, and we should wait for results
expect(screen.getByText("Search")).toBeDisabled();
expect(screen.getByTestId("spinner")).toBeVisible();
await waitFor(() => {
expect(screen.queryByTestId("spinner")).not.toBeInTheDocument();
});

// we should see an error page again
expect(screen.queryAllByTestId("repository-card")).toHaveLength(0);
});

it("should abort previous request when searching fast", async () => {
const searchClient = { search: jest.fn() };
render(
<RepositorySearchPageConnected searchClient={searchClient} />,
renderOptions
);

// we don't wait for results, we want to search again very soon
// change query to "12" and search again
userEvent.click(screen.getByDisplayValue("react"));
userEvent.keyboard("{selectall}12");
act(() => void userEvent.click(screen.getByText("Search")));

expect(
(searchClient.search.mock.calls[0][1] as AbortController).signal.aborted
).toBeTruthy();
expect(
(searchClient.search.mock.calls[1][1] as AbortController).signal.aborted
).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { render, screen, within } from "@testing-library/react";
import { RepositorySearchPagePure } from "./RepositorySearchPage.pure";
import userEvent from "@testing-library/user-event";
import { renderOptions } from "./__test__/test-common";

describe("RepositorySearchPage.pure", () => {
it("should render proper structure in loading mode", function () {
render(
<RepositorySearchPagePure
query="test query"
repositories={{ type: "pending" }}
onQueryChange={jest.fn()}
/>,
renderOptions
);
expect(screen.getByTestId("spinner")).toBeVisible();
});

it("should render proper structure in error mode", function () {
render(
<RepositorySearchPagePure
query="test query"
repositories={{ type: "error", message: "Test error" }}
onQueryChange={jest.fn()}
/>,
renderOptions
);
expect(screen.getByText(/Test error/)).toBeVisible();
});

it("should render proper structure in loaded mode, no results", function () {
render(
<RepositorySearchPagePure
query="test query"
repositories={{ type: "loaded", data: [] }}
onQueryChange={jest.fn()}
/>,
renderOptions
);
expect(screen.getByText("No repositories found")).toBeVisible();
});

it("should render proper structure in loaded mode, few results", function () {
render(
<RepositorySearchPagePure
query="test query"
repositories={{
type: "loaded",
data: [
{
id: 0,
name: "Foo",
description: "first repo",
linkUrl: "https://google.com",
owner: "Primo",
numStars: 10,
numForks: 100,
},
{
id: 1,
name: "Bar",
description: "second repo",
linkUrl: "https://google.com",
owner: "Secondo",
numStars: 20,
numForks: 200,
},
],
}}
onQueryChange={jest.fn()}
/>,
renderOptions
);

const cardViews = screen.getAllByTestId("repository-card");

const firstCardView = within(cardViews[0]);
expect(firstCardView.getByText(/Foo/)).toBeVisible();
expect(firstCardView.getByText("first repo")).toBeVisible();
expect(firstCardView.getByText(/Primo/)).toBeVisible();
expect(firstCardView.getByTitle("Stars and forks")).toHaveTextContent(
"10100"
);

const secondCardView = within(cardViews[1]);
expect(secondCardView.getByText(/Bar/)).toBeVisible();
expect(secondCardView.getByText("second repo")).toBeVisible();
expect(secondCardView.getByText(/Secondo/)).toBeVisible();
expect(secondCardView.getByTitle("Stars and forks")).toHaveTextContent(
"20200"
);
});

it("should show provided query in the search box", () => {
render(
<RepositorySearchPagePure
query="findme123"
repositories={{ type: "pending" }}
onQueryChange={jest.fn()}
/>,
renderOptions
);
expect(screen.getByDisplayValue("findme123")).toBeVisible();
});

it("should allow to write query and click search", () => {
const onSubmitMock = jest.fn();
render(
<RepositorySearchPagePure
query="react"
repositories={{ type: "pending" }}
onQueryChange={onSubmitMock}
/>,
renderOptions
);
userEvent.click(screen.getByDisplayValue("react"));
userEvent.keyboard("ish");
userEvent.click(screen.getByText("Search"));

expect(onSubmitMock).toBeCalledWith("reactish");
});

it("should allow to click search where the query is unchanged and there is an error", () => {
const onSubmitMock = jest.fn();
render(
<RepositorySearchPagePure
query="react"
repositories={{ type: "error", message: "try again" }}
onQueryChange={onSubmitMock}
/>,
renderOptions
);
userEvent.click(screen.getByText("Search"));

expect(onSubmitMock).toBeCalledWith("react");
});

it("should forbit to click search where the query is unchanged and there no error", () => {
const onSubmitMock = jest.fn();
render(
<RepositorySearchPagePure
query="react"
repositories={{ type: "loaded", data: [] }}
onQueryChange={onSubmitMock}
/>,
renderOptions
);
expect(screen.getByText("Search")).toBeDisabled();
userEvent.click(screen.getByText("Search")); // click anyway
expect(onSubmitMock).not.toBeCalled();
});

});

0 comments on commit 5d4db74

Please sign in to comment.